3131import org .xrpl .xrpl4j .client .JsonRpcClientErrorException ;
3232import org .xrpl .xrpl4j .crypto .keys .KeyPair ;
3333import org .xrpl .xrpl4j .crypto .keys .PublicKey ;
34+ import org .xrpl .xrpl4j .crypto .keys .Seed ;
3435import org .xrpl .xrpl4j .crypto .signing .MultiSignedTransaction ;
3536import org .xrpl .xrpl4j .crypto .signing .Signature ;
3637import org .xrpl .xrpl4j .crypto .signing .SingleSignedTransaction ;
4748import org .xrpl .xrpl4j .model .client .transactions .SubmitMultiSignedResult ;
4849import org .xrpl .xrpl4j .model .client .transactions .SubmitResult ;
4950import org .xrpl .xrpl4j .model .client .transactions .TransactionResult ;
51+ import org .xrpl .xrpl4j .model .flags .PaymentFlags ;
5052import org .xrpl .xrpl4j .model .flags .SponsorFlags ;
5153import org .xrpl .xrpl4j .model .flags .SponsorshipSetFlags ;
5254import org .xrpl .xrpl4j .model .flags .SponsorshipTransferFlags ;
55+ import org .xrpl .xrpl4j .model .ledger .AccountRootObject ;
5356import org .xrpl .xrpl4j .model .ledger .CheckObject ;
5457import org .xrpl .xrpl4j .model .ledger .SignerEntry ;
5558import org .xrpl .xrpl4j .model .ledger .SignerEntryWrapper ;
@@ -252,13 +255,14 @@ void testPreFundedFeeSponsorshipNoCosign() throws JsonRpcClientErrorException, J
252255
253256 // Sponsor creates SponsorshipSet without RequireSignForFee flag
254257 AccountInfoResult sponsorAccountInfo = scanForResult (() -> getValidatedAccountInfo (sponsorAddress ));
258+ XrpCurrencyAmount feeAmount = XrpCurrencyAmount .ofDrops (500000 );
255259
256260 SponsorshipSet sponsorshipSet = SponsorshipSet .builder ()
257261 .account (sponsorAddress )
258262 .fee (feeResult .drops ().openLedgerFee ())
259263 .sequence (sponsorAccountInfo .accountData ().sequence ())
260264 .sponsee (sponseeAddress )
261- .feeAmount (XrpCurrencyAmount . ofDrops ( 500000 ) )
265+ .feeAmount (feeAmount )
262266 .signingPublicKey (sponsorKeyPair .publicKey ())
263267 .build ();
264268
@@ -272,11 +276,13 @@ void testPreFundedFeeSponsorshipNoCosign() throws JsonRpcClientErrorException, J
272276
273277 // Sponsee submits payment using pre-funded fee (no sponsor signature needed)
274278 AccountInfoResult sponseeAccountInfo = scanForResult (() -> getValidatedAccountInfo (sponseeAddress ));
279+ XrpCurrencyAmount sponseeInitialBalance = sponseeAccountInfo .accountData ().balance ();
280+ XrpCurrencyAmount paymentAmount = XrpCurrencyAmount .ofDrops (50000 );
275281
276282 Payment payment = Payment .builder ()
277283 .account (sponseeAddress )
278284 .destination (destAddress )
279- .amount (XrpCurrencyAmount . ofDrops ( 50000 ) )
285+ .amount (paymentAmount )
280286 .fee (feeResult .drops ().openLedgerFee ())
281287 .sequence (sponseeAccountInfo .accountData ().sequence ())
282288 .sponsor (sponsorAddress )
@@ -290,6 +296,72 @@ void testPreFundedFeeSponsorshipNoCosign() throws JsonRpcClientErrorException, J
290296
291297 SubmitResult <Payment > paymentResult = xrplClient .submit (signedPayment );
292298 assertThat (paymentResult .engineResult ()).isEqualTo (SUCCESS_STATUS );
299+ scanForResult (() -> getValidatedTransaction (paymentResult .transactionResult ().hash (), Payment .class ));
300+
301+ // Assert the fee was paid from the pre-funded Sponsorship object, not the sponsee's own balance:
302+ // the sponsee's balance drops only by the payment amount, and the Sponsorship's FeeAmount decreases
303+ // by the network fee.
304+ XrpCurrencyAmount sponseeFinalBalance = scanForResult (
305+ () -> getValidatedAccountInfo (sponseeAddress )
306+ ).accountData ().balance ();
307+ XrpCurrencyAmount expectedSponseeBalance = XrpCurrencyAmount .ofDrops (
308+ sponseeInitialBalance .value ().longValue () - paymentAmount .value ().longValue ()
309+ );
310+ assertThat (sponseeFinalBalance ).isEqualTo (expectedSponseeBalance );
311+
312+ SponsorshipObject updatedSponsorship = getSponsorshipObject (sponsorAddress , sponseeAddress );
313+ XrpCurrencyAmount expectedFeeAmount = XrpCurrencyAmount .ofDrops (
314+ feeAmount .value ().longValue () - feeResult .drops ().openLedgerFee ().value ().longValue ()
315+ );
316+ assertThat (updatedSponsorship .feeAmount ()).hasValue (expectedFeeAmount );
317+ }
318+
319+ /**
320+ * Test: {@code Payment.tfSponsorCreatedAccount} sponsors a brand-new destination account.
321+ *
322+ * <ol>
323+ * <li>Alice sends a Payment with {@code tfSponsorCreatedAccount} set to a fresh, never-before-seen
324+ * destination address, with an {@code Amount} below the normal account reserve requirement
325+ * (allowed per spec section 11.3 when this flag is enabled).</li>
326+ * <li>Assert the new account was created and its {@code AccountRoot.Sponsor} field points to Alice.</li>
327+ * </ol>
328+ */
329+ @ Test
330+ void testSponsorCreatedAccountPayment () throws JsonRpcClientErrorException , JsonProcessingException {
331+ KeyPair aliceKeyPair = createRandomAccountEd25519 ();
332+ Address aliceAddress = aliceKeyPair .publicKey ().deriveAddress ();
333+
334+ // A fresh, never-funded keypair. This account does not yet exist on the ledger.
335+ KeyPair newAccountKeyPair = Seed .ed25519Seed ().deriveKeyPair ();
336+ Address newAccountAddress = newAccountKeyPair .publicKey ().deriveAddress ();
337+
338+ FeeResult feeResult = xrplClient .fee ();
339+ AccountInfoResult aliceAccountInfo = scanForResult (() -> getValidatedAccountInfo (aliceAddress ));
340+
341+ Payment sponsoredAccountCreate = Payment .builder ()
342+ .account (aliceAddress )
343+ .destination (newAccountAddress )
344+ .amount (XrpCurrencyAmount .ofDrops (1 )) // Below the normal reserve; allowed with this flag.
345+ .fee (feeResult .drops ().openLedgerFee ())
346+ .sequence (aliceAccountInfo .accountData ().sequence ())
347+ .flags (PaymentFlags .builder ().tfSponsorCreatedAccount (true ).build ())
348+ .signingPublicKey (aliceKeyPair .publicKey ())
349+ .build ();
350+
351+ SingleSignedTransaction <Payment > signedPayment = signatureService .sign (
352+ aliceKeyPair .privateKey (), sponsoredAccountCreate
353+ );
354+ SubmitResult <Payment > paymentResult = xrplClient .submit (signedPayment );
355+ assertThat (paymentResult .engineResult ()).isEqualTo (SUCCESS_STATUS );
356+ logInfo (sponsoredAccountCreate .transactionType (), paymentResult .transactionResult ().hash ());
357+
358+ scanForResult (() -> getValidatedTransaction (paymentResult .transactionResult ().hash (), Payment .class ));
359+
360+ // Assert the new account exists and is sponsored by Alice.
361+ AccountRootObject newAccountData = scanForResult (
362+ () -> getValidatedAccountInfo (newAccountAddress )
363+ ).accountData ();
364+ assertThat (newAccountData .sponsor ()).hasValue (aliceAddress );
293365 }
294366 }
295367
@@ -694,6 +766,19 @@ void testSponsorshipTransferSingleSigneeSingleSponsor() throws JsonRpcClientErro
694766 () -> getValidatedTransaction (finalTx .hash (), SponsorshipTransfer .class )
695767 );
696768 assertThat (validatedTx .metadata ().get ().transactionResult ()).isEqualTo (SUCCESS_STATUS );
769+
770+ // Assert the account-level reserve sponsorship was actually established on-ledger: the
771+ // sponsee's AccountRoot now names the new sponsor, and the new sponsor's SponsoringAccountCount
772+ // increased.
773+ AccountRootObject sponseeAccountData = scanForResult (
774+ () -> getValidatedAccountInfo (sponseeAddress )
775+ ).accountData ();
776+ assertThat (sponseeAccountData .sponsor ()).hasValue (newSponsorAddress );
777+
778+ AccountRootObject newSponsorAccountData = scanForResult (
779+ () -> getValidatedAccountInfo (newSponsorAddress )
780+ ).accountData ();
781+ assertThat (newSponsorAccountData .sponsoringAccountCount ()).hasValue (UnsignedInteger .ONE );
697782 }
698783
699784 /**
@@ -835,6 +920,19 @@ void testSponsorshipTransferMultiSponseeMultiSponsor() throws JsonRpcClientError
835920 () -> getValidatedTransaction (finalTx .hash (), SponsorshipTransfer .class )
836921 );
837922 assertThat (validatedTx .metadata ().get ().transactionResult ()).isEqualTo (SUCCESS_STATUS );
923+
924+ // Assert the account-level reserve sponsorship was actually established on-ledger: the
925+ // sponsee's AccountRoot now names the sponsor account, and the sponsor's SponsoringAccountCount
926+ // increased.
927+ AccountRootObject sponseeAccountData = scanForResult (
928+ () -> getValidatedAccountInfo (sponseeAddress )
929+ ).accountData ();
930+ assertThat (sponseeAccountData .sponsor ()).hasValue (sponsorAddress );
931+
932+ AccountRootObject sponsorAccountData = scanForResult (
933+ () -> getValidatedAccountInfo (sponsorAddress )
934+ ).accountData ();
935+ assertThat (sponsorAccountData .sponsoringAccountCount ()).hasValue (UnsignedInteger .ONE );
838936 }
839937 }
840938
0 commit comments