Skip to content

Commit df79914

Browse files
cybele-ripplePatel-Raj11sappenin
authored
Xls 68 sponsored fees reserves updated (#720)
* initial commit * add tests for IssueType * add NumberType tests * add tests * add DomainID to permission domain * add fixtures * rename NumberAmount to AssetAmount * add IssueType tests * import IT * add more tests * fix checkstyle warnings * add transactions, ledger objects and tests * address code review comments * add migration guide * remove unnecessary checks * small refactor * update migration guide * xrpl-ai suggestions * add helper methods for counterparty signing * add sorting support for multi-sign in CounterpartySignature * add tests for signing/multi-sign * add isNegative and isZero on AssetAmount * add LoanBrokerSetTest * add LoanBrokerDeleteTest * add computeLoanSetNetworkFees * add LoanBrokerCoverDepositTest * add LoanBrokerCoverWithdrawTest * add LoanBrokerCoverClawbackTest * add LoanSet test * add more tests * add Meta object tests * disable SingleAssetVaultIT on Testnet * refactor IT * fix checkstyle warnings * restore XrplEnvironment * add checks on AssetAmount * add signers length checks * increase coverage * fix checkstyle * initial commit XLS-0068 * added changes based on 2 agent reviews * remove superfluous files * remove config file * fix linting errors * fixed @beta tag * added wrappers for single signed and multi signed transactions * resolve error related to merge * enhanced test coverage * added test coverage * addressed PR comments * added validation checks * use xrpld:sponsor * fix combatibility issues * addressed PR comments * test fix: Enable SponsorshipIT and align with rippled XLS-68 sponsor branch * test fix: update SponsorshipFlagsTest to match lsf* flag values (0x10000/0x20000) * fix: restore legleux/xrpld:sponsor as RippledContainer image for CI * chore(test): use local xrpld:sponsor-local image for RippledContainer Swap the RippledContainer image from legleux/xrpld:sponsor (pulled from Docker Hub) to a locally-built xrpld:sponsor-local tag with PullPolicy.defaultPolicy(), so integration tests run against the locally-built rippled sponsor-branch image. * style: fix VariableDeclarationUsageDistance on charlieAddress in SponsorshipIT Move the charlieAddress declaration in testSponsorshipTransferReassign from the top of the method to immediately before its first use in Step 3, satisfying the checkstyle VariableDeclarationUsageDistance rule (distance was 6, max allowed 3). * style: fix VariableDeclarationUsageDistance on charlieKeyPair in SponsorshipIT Move the charlieKeyPair declaration in testSponsorshipTransferReassign from the top of the method to immediately before its first use in Step 3 (alongside charlieAddress), satisfying the checkstyle VariableDeclarationUsageDistance rule. * fix(test): revert RippledContainer to legleux/xrpld:sponsor for CI Restore the publicly-pullable legleux/xrpld:sponsor image and PullPolicy.alwaysPull() so integration tests run on GitHub Actions, which has no access to the locally-built xrpld:sponsor-local tag introduced in 2773b22. * fix(codec): add missing SponseeNode field to definitions.json Per XLS-0068, the Sponsorship ledger object carries a SponseeNode field (UInt64, nth=32). SponsorshipObject.java already serializes it via @JsonProperty("SponseeNode"), but the field was absent from definitions.json, so any binary-codec round-trip of a Sponsorship object emitted by rippled would fail to decode. Matches rippled source: sfields.macro line 155 TYPED_SFIELD(sfSponseeNode, UINT64, 32) * fix: remove direct docker-java-api import to fix undeclared dependency check Replaced explicit (Consumer<CreateContainerCmd>) cast with an inferred lambda so that the docker-java-api transitive dependency is no longer directly referenced in source, fixing the dependency:analyze-only failure. * fix: declare docker-java-api dependency and disable SponsorshipIT on devnet/testnet - Add docker-java-api as explicit test dependency in integration-tests pom.xml to satisfy dependency:analyze-only (RippledContainer directly uses CreateContainerCmd bytecode regardless of the source-level cast) - Annotate SponsorshipIT with @DisabledIf to skip on devnet/testnet/clio environments where the featureSponsorship amendment is not yet enabled, matching the pattern used by BatchTransactionIT, LendingProtocolIT, SingleAssetVaultIT, and XChainIT * fix: rebase Sponsor model onto current rippled#7350 wire format rippled's Sponsor implementation moved on significantly since this PR was written against #5887. Realigns the Java model with the current xrplf/sponsor branch: - SponsorshipSet/SponsorshipTransfer tx type codes: 86/85 -> 91/90 - ReserveCount renamed to RemainingOwnerCount and renumbered (72->73); SponsorFlags nth 73->74; ObjectID nth 39->41 - SponsorshipTransfer gains a Sponsee field for ending a third party's sponsorship, validated to only be set with tfSponsorshipEnd - SponsorshipSetFlags gains the missing Clear variants (tfSponsorshipClearRequireSignForFee/Reserve) - Sponsor field removed from OfferObject/NfTokenOfferObject/ TicketObject (excluded from rippled's current scope) and added to MpTokenObject/MpTokenIssuanceObject/SignerListObject (now in scope) - SponsorshipTransfer requires a SponsorSignature for account-level tfSponsorshipReassign, matching rippled's fix for the forced-transfer-without-consent vulnerability * fix: use fully-qualified Beta reference in Sponsor field javadoc The short {@link Beta} reference doesn't resolve in the Immutables-generated classes, which don't import com.google.common.annotations.Beta, breaking the javadoc:jar build (attach-javadocs). * fix: revert SponsorshipSet/SponsorshipTransfer type codes to 86/85 The CI-pinned rippled test image (legleux/xrpld:sponsor) is a snapshot that predates rippled's transaction-type renumbering on the xrplf/sponsor branch. It still expects SponsorshipSet=86 and SponsorshipTransfer=85, and rejects the current upstream values (91/90) with "Invalid transaction type". Reverting just the type codes restores compatibility with what CI can actually exercise today; the rest of the rebase (field renames, new flags, ledger-object scope, consent check) is unaffected since none of it is gated by tx type. * Address PR #720 review comments: signing, validation, and CI fixes - Move SponsorSignature to crypto.signing package (sappenin) - Move sponsorship field validation from SignatureUtils into a Transaction @Value.Check, and remove the now-redundant toSponsorSignableBytes/sponsorSign special-casing (sappenin) - Add missing @OverRide annotations on SponsorshipSetFlags (code quality bot) - Add missing unit test coverage for PaymentFlags.tfSponsorCreatedAccount and the new sponsor fields on PayChannelObject/EscrowObject/ RippleStateObject (sappenin) - Fix SponsorshipSet.flags() to return SponsorshipSetFlags instead of the generic TransactionFlags, and add field-level temMALFORMED-style validation to SponsorshipSet and SponsorshipTransfer matching rippled's merged preflight logic (Patel-Raj11) - Replace the personal legleux/xrpld:sponsor Docker image in RippledContainer with the official rippleci/xrpld:develop image, now that rippled#7350 has merged sponsor support into develop (sappenin, depthfirst-app) * Remove now-unused docker-java-api dependency The direct dependency was only needed for withCreateContainerCmdModifier's CreateContainerCmd type, which was removed along with the personal legleux/xrpld:sponsor image in the prior commit. * Fix stale Sponsor field wire-format numbers (nth) and naming Cross-checked every Sponsor-related SField against rippled's authoritative sfields.macro (XRPLF/rippled develop). Found and fixed: - ObjectID (SponsorshipTransfer): was nth 39, collided with main's new ReferenceHolding field during the recent merge; real value is 41 - SponsoredOwnerCount: 69 -> 70 - SponsoringOwnerCount: 70 -> 71 - SponsoringAccountCount: 71 -> 72 - ReserveCount: 72 -> 73, and renamed to RemainingOwnerCount (its real wire name) on both the SponsorshipSet transaction and the Sponsorship ledger object - SponsorFlags: 73 -> 74 These five UInt32 fields were each off by one because rippled has an unrelated sfConfidentialBalanceVersion field at nth 69 (from a separate, unimplemented-in-xrpl4j feature) that our numbering didn't account for. Without this fix, any binary-encoded transaction using these fields would carry the wrong field code and be rejected or misinterpreted by a real rippled node. * Remove stale Granular Permission TODO Rippled removed its SponsorFee/SponsorReserve GranularPermission implementation before merging Sponsor support into develop, so sponsorship no longer uses granular permissions. * Fix broken javadoc reference in SponsorSignature after package move The generated ImmutableSponsorSignature.java doesn't inherit SponsorSignature's imports, so the unqualified {@link Signer} reference (valid in the source file via its own import) failed to resolve in the generated class, breaking the javadoc build in CI. Fully-qualifying the reference fixes it regardless of imports. * Fix IT container startup: rename Batch amendment to BatchV1_1 The rippleci/xrpld:develop image renamed the Batch amendment to BatchV1_1, so rippled aborted on startup with 'Unknown feature: Batch', causing every integration test to fail with ExceptionInInitializerError in AbstractIT. Update xrpld.cfg to the current amendment name. * Align Sponsor definitions with rippleci/xrpld:develop The develop image assigns SponsorshipSet=91 and SponsorshipTransfer=90, but xrpl4j's definitions.json used 86/85 (which now map to ConfidentialMPTMergeInbox/ConfidentialMPTConvert in develop). This caused rippled to reject SponsorshipIT transactions with 'Field MPTokenIssuanceID is required but missing'. Also fix SponseeNode UInt64 nth (32->33) to match. * Correct SponsorshipTransfer flag bit values to match rippled develop rippled develop (commit 530e09d) defines the SponsorshipTransfer tf flags in the 0x0001xxxx range, but xrpl4j used 0x00000001/2/4. This caused develop to reject SponsorshipTransfer transactions with temINVALID_FLAG, failing 4 SponsorshipIT tests. Align the values: tfSponsorshipEnd 0x00000001 -> 0x00010000 tfSponsorshipCreate 0x00000002 -> 0x00020000 tfSponsorshipReassign 0x00000004 -> 0x00040000 Update the corresponding unit-test and JSON-fixture expectations. * Address PR #720 review comments: OwnerNode/SponseeNode types, SponsorshipSet tests, fee helper * Address remaining PR #720 review comments: inline SponsorshipValidations, regenerate definitions.json from server_definitions, add sponsored filter and ledger_entry lookup, relax SponsorSignature for inner-Batch txns * Move SponsorSignature to model.transactions, remove fabricated AccountRoot.Sponsor field, fix javadoc build failure * Fix SponsorshipLedgerEntryParams field name to match rippled's ledger_entry handler (sponsor, not owner) * Address PR #720 review comments: typed SponsorFlags, Sponsor on remaining ledger/Meta objects - Change Transaction.sponsorFlags() to return Optional<SponsorFlags> instead of Optional<UnsignedInteger>; simplify checkSponsorshipFields() and SponsorshipTransfer to use the typed flags directly, and update all call sites and tests. - Add Sponsor to AccountRootObject, CredentialObject, DelegateObject, SponsorshipObject and the corresponding Meta* objects; add HighSponsor/LowSponsor to MetaRippleStateObject to match RippleStateObject. Sponsor is a common ledger-entry field per the regenerated definitions.json, and only RippleState carries HighSponsor/LowSponsor. - Add testJsonWithSponsor round-trip coverage for each new ledger-object field. * Improve patch coverage for XLS-68 sponsorship code - Add unit tests for FeeUtils.computeSponsorshipTransferNetworkFees (null/limit guards and fee-multiplier math) — previously only exercised by integration tests. - Cover the multi-signer sort path in SponsorSignature.checkAndNormalize(). - Add SponsorshipTransfer/SponsorshipSet validation cases covering previously uncovered branches (end-with-sponsor, reserve-flag rules, delete-object field/flag restrictions, single require-sign-for flags). * Address PR #720 review round: spec-compliance fixes and broader Sponsor coverage Correctness (XLS-0068 section 8.3.1): - SponsorFlags.isValid() now rejects unknown flag bits (only {1,2,3} valid); add negative tests. - Transaction.checkSponsorshipFields() rejects a real SponsorSignature without Sponsor+SponsorFlags (all-or-nothing), and the invalid-flags message notes 'no other flags'. - Refactor sponsorSign() to reuse signatureHelper() instead of rebuilding via sign(). Ledger model: - Remove Sponsor from SponsorshipObject (not an allowed Sponsor-carrying type per spec). - Add Sponsor to the remaining spec section 4.3.1 allowed types: Ticket, Offer, NfTokenPage, NfTokenOffer, Amm, Bridge, XChainOwnedClaimId, XChainOwnedCreateAccountClaimId, Did, plus their Meta* counterparts (fixes TicketObject unused import). - Add SponsoredOwnerCount/SponsoringOwnerCount/SponsoringAccountCount to MetaAccountRootObject. - Expand JSON round-trip tests to cover the new Sponsor fields and AccountRoot counters. * Address remaining PR #720 IT/signing review comments (verified against live rippled develop) - SponsorshipIT: extend testPreFundedFeeSponsorshipNoCosign, testSponsorshipTransferSingleSigneeSingleSponsor, and testSponsorshipTransferMultiSponseeMultiSponsor to verify on-ledger state (balances, Sponsorship.feeAmount, AccountRoot.sponsor/sponsoringAccountCount) instead of only asserting tesSUCCESS. - SponsorshipIT: add testSponsorCreatedAccountPayment covering Payment.tfSponsorCreatedAccount, verified end-to-end against rippleci/xrpld:develop in standalone mode. - BcDerivedKeySignatureServiceTest: add known-good signature byte assertions to sponsorSignEd/Ec and sponsorMultiSignEd/Ec, matching the existing sibling multiSignEc pattern. All changes verified locally: 10/10 SponsorshipIT tests pass against rippleci/xrpld:develop (standalone), 36/36 BcDerivedKeySignatureServiceTest pass, full core suite (73473 tests) passes, checkstyle clean on both modules. * Remove Sponsor from ledger objects excluded by updated XLS-0068 v1 scope Per XRPL-Standards PR #564, spec section 4.3.1 now hard-scopes the common Sponsor field to a fixed v1 allow-list (AccountRoot, Check, Escrow, PayChannel, DepositPreauth, MPToken, MPTokenIssuance, Delegate, SignerList, Credential), with RippleState using its own HighSponsor/LowSponsor fields. All other ledger entry types, including the nine added in an earlier round (Offer, Ticket, NFTokenPage, NFTokenOffer, AMM, Bridge, XChainOwnedClaimID, XChainOwnedCreateAccountClaimID, DID), must not carry Sponsor. Removes sponsor() from these nine ledger objects and their Meta* counterparts, the corresponding JSON test coverage, and now-orphaned Beta/Address/Optional imports left behind by the removal. * fix: rename SponsorshipSet FeeAmount to FeeAmountDelta per rippled PR #335 rippled changed the SponsorshipSet transaction's fee field from an absolute sfFeeAmount to a signed sfFeeAmountDelta, letting sponsors top up or draw down a sponsorship's fee budget incrementally instead of overwriting it. Update the transaction model, validation (deltas may be negative but not zero), and tests to match; the Sponsorship ledger object's FeeAmount field is unaffected and remains absolute. * Fix FeeAmount JsonProperty mismatch in SponsorshipSet The JsonProperty annotation used "FeeAmountDelta" which does not match definitions.json, causing the field to be silently dropped from signed transaction bytes by the binary codec. * Align sponsorship validation with rippled PR #7350 Requires SponsorSignature for account-level tfSponsorshipCreate (not just tfSponsorshipReassign), restricts spfSponsorReserve to rippled's allow-listed transaction types, and disallows spfSponsorReserve combined with Delegate, matching the merged rippled implementation. * Align SponsorshipSet with rippled PR #335 (FeeAmountDelta/RemainingOwnerCountDelta) rippled introduced sfFeeAmountDelta and sfRemainingOwnerCountDelta as new, distinct SFields on SponsorshipSet, separate from the Sponsorship ledger entry's sfFeeAmount/sfRemainingOwnerCount. This SDK still serialized SponsorshipSet using the old field names/types, which definitions.json also never got the new field entries for, so signed transactions wouldn't wire-match a rippled node running the amendment. Also add the temINVALID/temREDUNDANT-equivalent preflight checks rippled added alongside the field rename. * ci: bump XRPLD_PRIVATE_VERSION to 3.3.0-rc7 * codec: add Int32 serialized type support RemainingOwnerCountDelta's Int32 sfield type had no codec implementation, so signing any SponsorshipSet carrying it threw "Unknown serialized type 'Int32'" (per kuan121's PR #720 review comment). Add Int32Type (signed 32-bit two's complement, same wire width as UInt32Type) and register it in SerializedType's type map. * fix: allow building an account-level SponsorshipTransfer without a SponsorSignature The co-signing flow for an account-level tfSponsorshipCreate/tfSponsorshipReassign is inherently two-phase: build the transaction without a SponsorSignature, have the new sponsor sign over that unsigned form, then rebuild with the resulting SponsorSignature attached. The model's check() previously required sponsorSignature() to already be present, which made it impossible to construct that necessary unsigned intermediate object at all. Drop the precondition; rippled is what rejects a final submission still missing the signature, same as how transactionSignature() itself is never required at construction elsewhere in this codebase. * fix: catch NumberFormatException in Int32Type#toJson() Wraps hex parsing in a try/catch and rethrows as IllegalStateException with the offending hex value, per code-quality bot suggestion. * docs: Sponsor amendment is now supported on public rippled develop, not just a private dev build * Add MetaSponsorshipObject and dedupe FeeAmount/MaxFee in definitions.json Every ledger entry has a Meta*Object counterpart except UnknownLedgerObject (intentional) and Sponsorship, which was missing one; add it, mirroring the ledger entry's fields, and register it in MetaLedgerEntryType. Also remove a byte-for-byte duplicate FeeAmount/MaxFee FIELDS entry in definitions.json (same nth/type), keeping the copy correctly grouped in ascending nth order alongside FeeAmountDelta. --------- Co-authored-by: Raj Patel <rajp@ripple.com> Co-authored-by: David Fuelling <sappenin@gmail.com>
1 parent ebce35d commit df79914

103 files changed

Lines changed: 8379 additions & 40 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@
330330
<configuration>
331331
<source>${maven.compiler.source}</source>
332332
<target>${maven.compiler.target}</target>
333+
<!-- Prevents annotation processor ordering issues with Immutables -->
334+
<useIncrementalCompilation>false</useIncrementalCompilation>
333335

334336
<annotationProcessorPaths>
335337
<path>

xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsResult;
5454
import org.xrpl.xrpl4j.model.client.accounts.AccountOffersRequestParams;
5555
import org.xrpl.xrpl4j.model.client.accounts.AccountOffersResult;
56+
import org.xrpl.xrpl4j.model.client.accounts.AccountSponsoringRequestParams;
57+
import org.xrpl.xrpl4j.model.client.accounts.AccountSponsoringResult;
5658
import org.xrpl.xrpl4j.model.client.accounts.AccountTransactionsRequestParams;
5759
import org.xrpl.xrpl4j.model.client.accounts.AccountTransactionsResult;
5860
import org.xrpl.xrpl4j.model.client.accounts.GatewayBalancesRequestParams;
@@ -620,6 +622,31 @@ public AccountOffersResult accountOffers(AccountOffersRequestParams params) thro
620622
return jsonRpcClient.send(request, AccountOffersResult.class);
621623
}
622624

625+
/**
626+
* Get the {@link AccountSponsoringResult} for the account specified in {@code params} by making an account_sponsoring
627+
* method call.
628+
*
629+
* <p>This method will be marked {@link Beta} until the featureSponsorship amendment is enabled on mainnet.
630+
* Its API is subject to change.</p>
631+
*
632+
* @param params The {@link AccountSponsoringRequestParams} to send in the request.
633+
*
634+
* @return The {@link AccountSponsoringResult} returned by the account_sponsoring method call.
635+
*
636+
* @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error.
637+
*
638+
* @see "https://github.com/XRPLF/XRPL-Standards/blob/master/XLS-0068-sponsored-fees-and-reserves/README.md"
639+
*/
640+
@Beta
641+
public AccountSponsoringResult accountSponsoring(AccountSponsoringRequestParams params)
642+
throws JsonRpcClientErrorException {
643+
JsonRpcRequest request = JsonRpcRequest.builder()
644+
.method(XrplMethods.ACCOUNT_SPONSORING)
645+
.addParams(params)
646+
.build();
647+
return jsonRpcClient.send(request, AccountSponsoringResult.class);
648+
}
649+
623650
/**
624651
* Indicates whether one account is authorized to send payments directly to another.
625652
*

xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsResult;
6666
import org.xrpl.xrpl4j.model.client.accounts.AccountOffersRequestParams;
6767
import org.xrpl.xrpl4j.model.client.accounts.AccountOffersResult;
68+
import org.xrpl.xrpl4j.model.client.accounts.AccountSponsoringRequestParams;
69+
import org.xrpl.xrpl4j.model.client.accounts.AccountSponsoringResult;
6870
import org.xrpl.xrpl4j.model.client.accounts.AccountTransactionsRequestParams;
6971
import org.xrpl.xrpl4j.model.client.accounts.AccountTransactionsResult;
7072
import org.xrpl.xrpl4j.model.client.accounts.GatewayBalancesRequestParams;
@@ -993,6 +995,20 @@ public void accountLines() throws JsonRpcClientErrorException {
993995
assertThat(jsonRpcRequestArgumentCaptor.getValue().params().get(0)).isEqualTo(accountLinesRequestParams);
994996
}
995997

998+
@Test
999+
public void accountSponsoring() throws JsonRpcClientErrorException {
1000+
AccountSponsoringRequestParams accountSponsoringRequestParams = AccountSponsoringRequestParams.builder()
1001+
.account(Address.of("rDgZZ3wyprx4ZqrGQUkquE9Fs2Xs8XBcdw"))
1002+
.ledgerSpecifier(LedgerSpecifier.CURRENT)
1003+
.build();
1004+
xrplClient.accountSponsoring(accountSponsoringRequestParams);
1005+
1006+
ArgumentCaptor<JsonRpcRequest> jsonRpcRequestArgumentCaptor = ArgumentCaptor.forClass(JsonRpcRequest.class);
1007+
verify(jsonRpcClientMock).send(jsonRpcRequestArgumentCaptor.capture(), eq(AccountSponsoringResult.class));
1008+
assertThat(jsonRpcRequestArgumentCaptor.getValue().method()).isEqualTo(XrplMethods.ACCOUNT_SPONSORING);
1009+
assertThat(jsonRpcRequestArgumentCaptor.getValue().params().get(0)).isEqualTo(accountSponsoringRequestParams);
1010+
}
1011+
9961012
@Test
9971013
public void channelVerify() throws JsonRpcClientErrorException {
9981014
ChannelVerifyRequestParams channelVerifyRequestParams = ChannelVerifyRequestParams.builder()
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package org.xrpl.xrpl4j.codec.binary.types;
2+
3+
/*-
4+
* ========================LICENSE_START=================================
5+
* xrpl4j :: binary-codec
6+
* %%
7+
* Copyright (C) 2020 - 2022 XRPL Foundation and its contributors
8+
* %%
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =========================LICENSE_END==================================
21+
*/
22+
23+
import com.fasterxml.jackson.databind.JsonNode;
24+
import com.fasterxml.jackson.databind.node.IntNode;
25+
import org.xrpl.xrpl4j.codec.addresses.ByteUtils;
26+
import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray;
27+
import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser;
28+
29+
import java.math.BigInteger;
30+
31+
/**
32+
* Codec for the XRPL Int32 type, a signed 32-bit integer encoded on the wire using two's complement, the same
33+
* 4-byte-wide encoding as {@link UInt32Type}.
34+
*/
35+
public class Int32Type extends SerializedType<Int32Type> {
36+
37+
private static final BigInteger UINT32_MASK = BigInteger.valueOf(0xFFFFFFFFL);
38+
private static final int HEX_LENGTH = 8;
39+
40+
/**
41+
* No-args constructor, defaulting to a value of {@code 0}.
42+
*/
43+
public Int32Type() {
44+
this(0);
45+
}
46+
47+
/**
48+
* Required-args constructor.
49+
*
50+
* @param value The signed {@code int} value that this {@link Int32Type} should encode.
51+
*/
52+
public Int32Type(int value) {
53+
super(UnsignedByteArray.fromHex(
54+
ByteUtils.padded(BigInteger.valueOf(value).and(UINT32_MASK).toString(16), HEX_LENGTH)
55+
));
56+
}
57+
58+
private Int32Type(UnsignedByteArray bytes) {
59+
super(bytes);
60+
}
61+
62+
@Override
63+
public Int32Type fromParser(BinaryParser parser) {
64+
return new Int32Type(parser.read(4));
65+
}
66+
67+
@Override
68+
public Int32Type fromJson(JsonNode value) {
69+
return new Int32Type(value.asInt());
70+
}
71+
72+
@Override
73+
public JsonNode toJson() {
74+
String hex = toHex();
75+
try {
76+
return new IntNode((int) Long.parseLong(hex, 16));
77+
} catch (NumberFormatException e) {
78+
throw new IllegalStateException("Invalid hex value for Int32Type: " + hex, e);
79+
}
80+
}
81+
}

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/SerializedType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public abstract class SerializedType<T extends SerializedType<T>> {
5252
.put("Hash160", () -> new Hash160Type())
5353
.put("Hash192", () -> new UInt192Type())
5454
.put("Hash256", () -> new Hash256Type())
55+
.put("Int32", () -> new Int32Type())
5556
.put("PathSet", () -> new PathSetType())
5657
.put("STArray", () -> new STArrayType())
5758
.put("STObject", () -> new STObjectType())

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureService.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ public Signature counterpartyMultiSign(final P privateKeyable, final LoanSet tra
138138
return abstractTransactionSigner.counterpartyMultiSign(privateKeyable, transaction);
139139
}
140140

141+
@Override
142+
public <T extends Transaction> Signature sponsorSign(final P privateKeyable, final T transaction) {
143+
return abstractTransactionSigner.sponsorSign(privateKeyable, transaction);
144+
}
145+
146+
@Override
147+
public <T extends Transaction> Signature sponsorMultiSign(final P privateKeyable, final T transaction) {
148+
return abstractTransactionSigner.sponsorMultiSign(privateKeyable, transaction);
149+
}
150+
141151
@Override
142152
public <T extends Transaction> boolean verify(final Signer signer, final T unsignedTransaction) {
143153
return abstractTransactionVerifier.verify(signer, unsignedTransaction);

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractTransactionSigner.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,33 @@ public Signature counterpartyMultiSign(final P privateKeyable, final LoanSet tra
143143
return this.signatureHelper(privateKeyable, signableTransactionBytes);
144144
}
145145

146+
@Override
147+
public <T extends Transaction> Signature sponsorSign(final P privateKeyable, final T transaction) {
148+
Objects.requireNonNull(privateKeyable);
149+
Objects.requireNonNull(transaction);
150+
151+
// Per the rippled implementation of the Sponsorship amendment, sponsor single-signing uses the same
152+
// HashPrefix::txSign (STX, 0x53545800) prefix and serialization as regular single-signing. Domain separation
153+
// between the account-owner and sponsor roles is not required at the signing-bytes level because the resulting
154+
// signatures are placed in distinct transaction fields (TxnSignature vs SponsorSignature.TxnSignature), and the
155+
// account-owner and sponsor use different key pairs. So this can safely reuse the regular signing path.
156+
return this.signatureHelper(privateKeyable, transaction);
157+
}
158+
159+
@Override
160+
public <T extends Transaction> Signature sponsorMultiSign(final P privateKeyable, final T transaction) {
161+
Objects.requireNonNull(privateKeyable);
162+
Objects.requireNonNull(transaction);
163+
164+
// Sponsor multi-signing preserves the first-party signer's SigningPubKey in the signed data.
165+
// This differs from regular multi-signing which clears the SigningPubKey.
166+
final Address address = derivePublicKey(privateKeyable).deriveAddress();
167+
final UnsignedByteArray signableTransactionBytes = this.signatureUtils.toSponsorMultiSignableBytes(
168+
transaction, address
169+
);
170+
return this.signatureHelper(privateKeyable, signableTransactionBytes);
171+
}
172+
146173
/**
147174
* Helper to serialize a {@link Transaction} to signable bytes and then generate a {@link Signature}.
148175
*

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public SignatureUtils(final ObjectMapper objectMapper, final XrplBinaryCodec bin
7777
*/
7878
public UnsignedByteArray toSignableBytes(final Transaction transaction) {
7979
Objects.requireNonNull(transaction);
80+
8081
try {
8182
final String unsignedJson = objectMapper.writeValueAsString(transaction);
8283
final String unsignedBinaryHex = binaryCodec.encodeForSigning(unsignedJson);
@@ -217,4 +218,37 @@ public UnsignedByteArray toMultiSignableInnerBytes(
217218
Objects.requireNonNull(nestedSignerAddress);
218219
return binaryCodec.encodeForBatchInnerMultiSigning(batch, batchSignerAddress, nestedSignerAddress);
219220
}
221+
222+
/**
223+
* Helper method to convert a {@link Transaction} into bytes that can be multi-signed by a sponsor.
224+
*
225+
* <p>Unlike {@link #toMultiSignableBytes(Transaction, Address)}, this method preserves the existing
226+
* {@code SigningPubKey} field in the encoded bytes. This is necessary when a sponsor multi-signs a transaction
227+
* where the first-party signer's {@code SigningPubKey} must remain intact in the signed data.</p>
228+
*
229+
* <p>This method will be marked {@link Beta} until the featureSponsorship amendment is enabled on mainnet.
230+
* Its API is subject to change.</p>
231+
*
232+
* @param transaction A {@link Transaction} to be sponsor multi-signed.
233+
* @param signerAddress The {@link Address} of the sponsor signer.
234+
*
235+
* @return An {@link UnsignedByteArray}.
236+
*/
237+
@Beta
238+
public UnsignedByteArray toSponsorMultiSignableBytes(final Transaction transaction, final Address signerAddress) {
239+
Objects.requireNonNull(transaction);
240+
Objects.requireNonNull(signerAddress);
241+
242+
try {
243+
final String unsignedJson = objectMapper.writeValueAsString(transaction);
244+
final String unsignedBinaryHex = binaryCodec.encodeForMultiSigningWithSigningPubKey(
245+
unsignedJson, signerAddress.value()
246+
);
247+
return UnsignedByteArray.fromHex(unsignedBinaryHex);
248+
} catch (JsonProcessingException e) {
249+
throw new RuntimeException(e.getMessage(), e);
250+
}
251+
}
252+
220253
}
254+

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/TransactionSigner.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,50 @@ public interface TransactionSigner<P extends PrivateKeyable> {
179179
@Beta
180180
Signature counterpartyMultiSign(P privateKeyable, LoanSet transaction);
181181

182+
/**
183+
* Obtain a sponsor single-signature for the supplied transaction. Per rippled's Sponsorship amendment
184+
* implementation, the sponsor signs the same serialized bytes (using the {@code STX} / 0x53545800 prefix) as the
185+
* account-owner. Domain separation is achieved structurally: the sponsor's signature is placed into the
186+
* {@link Transaction#sponsorSignature()} field rather than the transaction's {@code TxnSignature}, and the
187+
* account-owner and sponsor use different key pairs.
188+
*
189+
* <p>This method returns only the raw {@link Signature} rather than a {@link SingleSignedTransaction}
190+
* wrapper.</p>
191+
*
192+
* <p>This method will be marked {@link Beta} until the featureSponsorship amendment is enabled on mainnet.
193+
* Its API is subject to change.</p>
194+
*
195+
* @param privateKeyable The {@link P} used to sign {@code transaction}.
196+
* @param transaction The {@link Transaction} to sign as the sponsor.
197+
* @param <T> The type of the transaction to be signed.
198+
*
199+
* @return A {@link Signature} for the sponsor.
200+
*/
201+
@Beta
202+
<T extends Transaction> Signature sponsorSign(P privateKeyable, T transaction);
203+
204+
/**
205+
* Obtain a sponsor multi-signature for the supplied transaction. Unlike
206+
* {@link #multiSign(PrivateKeyable, Transaction)}, this method does <b>not</b> clear the {@code SigningPubKey}
207+
* field, preserving the first-party signer's public key in the signed data. The resulting bytes use the same
208+
* multi-signing prefix ({@code SMT\0}) and the sponsor signer's account ID suffix.
209+
*
210+
* <p>This is necessary for sponsored transactions where the sponsor must co-sign without overwriting
211+
* the transaction sender's signature. The sponsor's multi-signature is placed in the
212+
* {@link Transaction#sponsorSignature()} {@code Signers} array.</p>
213+
*
214+
* <p>This method will be marked {@link Beta} until the featureSponsorship amendment is enabled on mainnet.
215+
* Its API is subject to change.</p>
216+
*
217+
* @param privateKeyable The {@link P} used to sign {@code transaction}.
218+
* @param transaction The {@link Transaction} to sponsor multi-sign.
219+
* @param <T> The type of the transaction to be signed.
220+
*
221+
* @return A {@link Signature} for the sponsor signer.
222+
*/
223+
@Beta
224+
<T extends Transaction> Signature sponsorMultiSign(P privateKeyable, T transaction);
225+
182226
/**
183227
* Obtain a signature for the supplied unsigned transaction using the supplied {@link P}.
184228
* <p>

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/BcDerivedKeySignatureService.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,24 @@ public Signature counterpartyMultiSign(final PrivateKeyReference privateKeyRefer
178178
return getTransactionSigner(privateKeyReference).counterpartyMultiSign(transaction);
179179
}
180180

181+
@Override
182+
public <T extends Transaction> Signature sponsorSign(
183+
final PrivateKeyReference privateKeyReference, final T transaction
184+
) {
185+
Objects.requireNonNull(privateKeyReference);
186+
Objects.requireNonNull(transaction);
187+
return getTransactionSigner(privateKeyReference).sponsorSign(transaction);
188+
}
189+
190+
@Override
191+
public <T extends Transaction> Signature sponsorMultiSign(
192+
final PrivateKeyReference privateKeyReference, final T transaction
193+
) {
194+
Objects.requireNonNull(privateKeyReference);
195+
Objects.requireNonNull(transaction);
196+
return getTransactionSigner(privateKeyReference).sponsorMultiSign(transaction);
197+
}
198+
181199
@Override
182200
public <T extends Transaction> Signer multiSignToSigner(PrivateKeyReference privateKeyable, T transaction) {
183201
return getTransactionSigner(privateKeyable).multiSignToSigner(transaction);
@@ -372,6 +390,14 @@ public final Signature counterpartyMultiSign(final LoanSet transaction) {
372390
return bcSignatureService.counterpartyMultiSign(this.privateKey, transaction);
373391
}
374392

393+
public <T extends Transaction> Signature sponsorSign(final T transaction) {
394+
return bcSignatureService.sponsorSign(this.privateKey, transaction);
395+
}
396+
397+
public <T extends Transaction> Signature sponsorMultiSign(final T transaction) {
398+
return bcSignatureService.sponsorMultiSign(this.privateKey, transaction);
399+
}
400+
375401
public PublicKey getPublicKey() {
376402
return BcKeyUtils.toPublicKey(this.privateKey);
377403
}

0 commit comments

Comments
 (0)