Skip to content

Commit c990e37

Browse files
committed
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).
1 parent fdc8710 commit c990e37

4 files changed

Lines changed: 279 additions & 0 deletions

File tree

xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/fees/FeeUtilsTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static org.xrpl.xrpl4j.model.client.fees.FeeUtils.computeLoanSetNetworkFees;
2828
import static org.xrpl.xrpl4j.model.client.fees.FeeUtils.computeMultisigNetworkFees;
2929
import static org.xrpl.xrpl4j.model.client.fees.FeeUtils.computeNetworkFees;
30+
import static org.xrpl.xrpl4j.model.client.fees.FeeUtils.computeSponsorshipTransferNetworkFees;
3031
import static org.xrpl.xrpl4j.model.transactions.CurrencyAmount.MAX_XRP;
3132
import static org.xrpl.xrpl4j.model.transactions.CurrencyAmount.MAX_XRP_IN_DROPS;
3233

@@ -865,6 +866,79 @@ void testComputeLoanSetNetworkFeesWithOneCounterpartySigner() {
865866
assertThat(result.feeHigh()).isEqualTo(XrpCurrencyAmount.ofDrops(20000));
866867
}
867868

869+
// /////////////////
870+
// computeSponsorshipTransferNetworkFees
871+
// /////////////////
872+
873+
@Test
874+
void testComputeSponsorshipTransferNetworkFeesNullInputs() {
875+
assertThrows(NullPointerException.class,
876+
() -> computeSponsorshipTransferNetworkFees(null, UnsignedInteger.ZERO, UnsignedInteger.ZERO));
877+
assertThrows(NullPointerException.class,
878+
() -> computeSponsorshipTransferNetworkFees(mock(FeeResult.class), null, UnsignedInteger.ZERO));
879+
assertThrows(NullPointerException.class,
880+
() -> computeSponsorshipTransferNetworkFees(mock(FeeResult.class), UnsignedInteger.ZERO, null));
881+
}
882+
883+
@Test
884+
void testComputeSponsorshipTransferNetworkFeesSponseeSignersExceedsLimit() {
885+
FeeResult feeResult = feeResultBuilder().build();
886+
assertThrows(IllegalArgumentException.class,
887+
() -> computeSponsorshipTransferNetworkFees(feeResult, UnsignedInteger.valueOf(33), UnsignedInteger.ZERO));
888+
}
889+
890+
@Test
891+
void testComputeSponsorshipTransferNetworkFeesSponsorSignersExceedsLimit() {
892+
FeeResult feeResult = feeResultBuilder().build();
893+
assertThrows(IllegalArgumentException.class,
894+
() -> computeSponsorshipTransferNetworkFees(feeResult, UnsignedInteger.ZERO, UnsignedInteger.valueOf(33)));
895+
}
896+
897+
@Test
898+
void testComputeSponsorshipTransferNetworkFeesAtSignerLimit() {
899+
FeeResult feeResult = feeResultBuilder().build();
900+
// 32 is the max for each party — should not throw
901+
assertThat(
902+
computeSponsorshipTransferNetworkFees(feeResult, UnsignedInteger.valueOf(32), UnsignedInteger.valueOf(32))
903+
).isNotNull();
904+
}
905+
906+
@Test
907+
void testComputeSponsorshipTransferNetworkFeesSingleSignBothParties() {
908+
// Multiplier: (1 + 0 + 0) = 1
909+
FeeResult feeResult = feeResultBuilder().build();
910+
ComputedNetworkFees result = computeSponsorshipTransferNetworkFees(
911+
feeResult, UnsignedInteger.ZERO, UnsignedInteger.ZERO
912+
);
913+
assertThat(result.feeLow()).isEqualTo(XrpCurrencyAmount.ofDrops(1000));
914+
assertThat(result.feeMedium()).isEqualTo(XrpCurrencyAmount.ofDrops(5008));
915+
assertThat(result.feeHigh()).isEqualTo(XrpCurrencyAmount.ofDrops(10000));
916+
}
917+
918+
@Test
919+
void testComputeSponsorshipTransferNetworkFeesMultiSignSponsee() {
920+
// Multiplier: (1 + 2 + 0) = 3
921+
FeeResult feeResult = feeResultBuilder().build();
922+
ComputedNetworkFees result = computeSponsorshipTransferNetworkFees(
923+
feeResult, UnsignedInteger.valueOf(2), UnsignedInteger.ZERO
924+
);
925+
assertThat(result.feeLow()).isEqualTo(XrpCurrencyAmount.ofDrops(3000));
926+
assertThat(result.feeMedium()).isEqualTo(XrpCurrencyAmount.ofDrops(15024));
927+
assertThat(result.feeHigh()).isEqualTo(XrpCurrencyAmount.ofDrops(30000));
928+
}
929+
930+
@Test
931+
void testComputeSponsorshipTransferNetworkFeesMultiSignBothParties() {
932+
// Multiplier: (1 + 2 + 2) = 5
933+
FeeResult feeResult = feeResultBuilder().build();
934+
ComputedNetworkFees result = computeSponsorshipTransferNetworkFees(
935+
feeResult, UnsignedInteger.valueOf(2), UnsignedInteger.valueOf(2)
936+
);
937+
assertThat(result.feeLow()).isEqualTo(XrpCurrencyAmount.ofDrops(5000));
938+
assertThat(result.feeMedium()).isEqualTo(XrpCurrencyAmount.ofDrops(25040));
939+
assertThat(result.feeHigh()).isEqualTo(XrpCurrencyAmount.ofDrops(50000));
940+
}
941+
868942
private ImmutableFeeResult.Builder feeResultBuilder() {
869943
return FeeResult.builder()
870944
.currentLedgerSize(UnsignedInteger.valueOf(56))

xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/SponsorSignatureTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@
2424
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2525

2626
import org.junit.jupiter.api.Test;
27+
import org.xrpl.xrpl4j.codec.addresses.AddressCodec;
2728
import org.xrpl.xrpl4j.crypto.keys.PublicKey;
2829
import org.xrpl.xrpl4j.crypto.signing.Signature;
2930

31+
import java.math.BigInteger;
32+
import java.util.Arrays;
3033
import java.util.Collections;
34+
import java.util.List;
3135

3236
/**
3337
* Unit tests for {@link SponsorSignature}.
@@ -76,6 +80,36 @@ public void buildWithMultiSignature() {
7680
assertThat(signature.signers().get()).hasSize(1);
7781
}
7882

83+
@Test
84+
public void buildWithMultipleSignersSortsByAccountId() {
85+
Signer signerA = Signer.builder()
86+
.account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
87+
.signingPublicKey(PublicKey.fromBase16EncodedPublicKey(TEST_PUBLIC_KEY))
88+
.transactionSignature(Signature.fromBase16(TEST_SIGNATURE))
89+
.build();
90+
Signer signerB = Signer.builder()
91+
.account(Address.of("rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY"))
92+
.signingPublicKey(PublicKey.fromBase16EncodedPublicKey(TEST_PUBLIC_KEY))
93+
.transactionSignature(Signature.fromBase16(TEST_SIGNATURE))
94+
.build();
95+
96+
SponsorSignature signature = SponsorSignature.builder()
97+
.signingPublicKey(PublicKey.MULTI_SIGN_PUBLIC_KEY)
98+
.signers(Arrays.asList(SignerWrapper.of(signerA), SignerWrapper.of(signerB)))
99+
.build();
100+
101+
assertThat(signature.signers()).isPresent();
102+
List<SignerWrapper> signers = signature.signers().get();
103+
assertThat(signers).hasSize(2);
104+
105+
// checkAndNormalize() must sort the signers ascending by decoded AccountID.
106+
BigInteger first = new BigInteger(
107+
AddressCodec.getInstance().decodeAccountId(signers.get(0).signer().account()).hexValue(), 16);
108+
BigInteger second = new BigInteger(
109+
AddressCodec.getInstance().decodeAccountId(signers.get(1).signer().account()).hexValue(), 16);
110+
assertThat(first).isLessThan(second);
111+
}
112+
79113
@Test
80114
public void buildWithBothSignatureTypesFails() {
81115
assertThatThrownBy(() ->

xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/SponsorshipSetTest.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,106 @@ public void requireAndClearSignForReserveBothSetFails() {
166166
.hasMessageContaining("tfRequireSignForReserve and tfClearRequireSignForReserve must not both be set");
167167
}
168168

169+
@Test
170+
public void requireSignForFeeOnlySucceeds() {
171+
SponsorshipSet set = SponsorshipSet.builder()
172+
.account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
173+
.fee(XrpCurrencyAmount.ofDrops(10))
174+
.sequence(UnsignedInteger.ONE)
175+
.sponsee(Address.of("rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY"))
176+
.flags(SponsorshipSetFlags.builder().tfRequireSignForFee().build())
177+
.build();
178+
179+
assertThat(set.flags().tfRequireSignForFee()).isTrue();
180+
assertThat(set.flags().tfClearRequireSignForFee()).isFalse();
181+
}
182+
183+
@Test
184+
public void requireSignForReserveOnlySucceeds() {
185+
SponsorshipSet set = SponsorshipSet.builder()
186+
.account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
187+
.fee(XrpCurrencyAmount.ofDrops(10))
188+
.sequence(UnsignedInteger.ONE)
189+
.sponsee(Address.of("rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY"))
190+
.flags(SponsorshipSetFlags.builder().tfRequireSignForReserve().build())
191+
.build();
192+
193+
assertThat(set.flags().tfRequireSignForReserve()).isTrue();
194+
assertThat(set.flags().tfClearRequireSignForReserve()).isFalse();
195+
}
196+
197+
@Test
198+
public void deleteObjectWithRequireSignForReserveFlagFails() {
199+
assertThatThrownBy(() ->
200+
SponsorshipSet.builder()
201+
.account(Address.of("rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY"))
202+
.fee(XrpCurrencyAmount.ofDrops(10))
203+
.sequence(UnsignedInteger.ONE)
204+
.counterpartySponsor(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
205+
.flags(SponsorshipSetFlags.builder().tfDeleteObject().tfRequireSignForReserve().build())
206+
.build()
207+
).isInstanceOf(IllegalStateException.class)
208+
.hasMessageContaining("must not set any RequireSignFor*/ClearRequireSignFor* flags");
209+
}
210+
211+
@Test
212+
public void deleteObjectWithClearRequireSignForFeeFlagFails() {
213+
assertThatThrownBy(() ->
214+
SponsorshipSet.builder()
215+
.account(Address.of("rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY"))
216+
.fee(XrpCurrencyAmount.ofDrops(10))
217+
.sequence(UnsignedInteger.ONE)
218+
.counterpartySponsor(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
219+
.flags(SponsorshipSetFlags.builder().tfDeleteObject().tfClearRequireSignForFee().build())
220+
.build()
221+
).isInstanceOf(IllegalStateException.class)
222+
.hasMessageContaining("must not set any RequireSignFor*/ClearRequireSignFor* flags");
223+
}
224+
225+
@Test
226+
public void deleteObjectWithClearRequireSignForReserveFlagFails() {
227+
assertThatThrownBy(() ->
228+
SponsorshipSet.builder()
229+
.account(Address.of("rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY"))
230+
.fee(XrpCurrencyAmount.ofDrops(10))
231+
.sequence(UnsignedInteger.ONE)
232+
.counterpartySponsor(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
233+
.flags(SponsorshipSetFlags.builder().tfDeleteObject().tfClearRequireSignForReserve().build())
234+
.build()
235+
).isInstanceOf(IllegalStateException.class)
236+
.hasMessageContaining("must not set any RequireSignFor*/ClearRequireSignFor* flags");
237+
}
238+
239+
@Test
240+
public void deleteObjectWithMaxFeeFails() {
241+
assertThatThrownBy(() ->
242+
SponsorshipSet.builder()
243+
.account(Address.of("rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY"))
244+
.fee(XrpCurrencyAmount.ofDrops(10))
245+
.sequence(UnsignedInteger.ONE)
246+
.counterpartySponsor(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
247+
.maxFee(XrpCurrencyAmount.ofDrops(100))
248+
.flags(SponsorshipSetFlags.builder().tfDeleteObject().build())
249+
.build()
250+
).isInstanceOf(IllegalStateException.class)
251+
.hasMessageContaining("must not include FeeAmount, MaxFee, or RemainingOwnerCount");
252+
}
253+
254+
@Test
255+
public void deleteObjectWithRemainingOwnerCountFails() {
256+
assertThatThrownBy(() ->
257+
SponsorshipSet.builder()
258+
.account(Address.of("rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY"))
259+
.fee(XrpCurrencyAmount.ofDrops(10))
260+
.sequence(UnsignedInteger.ONE)
261+
.counterpartySponsor(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
262+
.remainingOwnerCount(UnsignedInteger.valueOf(5))
263+
.flags(SponsorshipSetFlags.builder().tfDeleteObject().build())
264+
.build()
265+
).isInstanceOf(IllegalStateException.class)
266+
.hasMessageContaining("must not include FeeAmount, MaxFee, or RemainingOwnerCount");
267+
}
268+
169269
@Test
170270
public void negativeFeeAmountFails() {
171271
assertThatThrownBy(() ->

xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/SponsorshipTransferTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,77 @@ public void buildReassignSponsorship() {
6767
assertThat(transfer.flags().tfSponsorshipReassign()).isTrue();
6868
}
6969

70+
@Test
71+
public void endSponsorshipWithSponsorPresentFails() {
72+
assertThatThrownBy(() -> SponsorshipTransfer.builder()
73+
.account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
74+
.fee(XrpCurrencyAmount.ofDrops(10))
75+
.sequence(UnsignedInteger.ONE)
76+
.objectId(Hash256.of("E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"))
77+
.sponsor(Address.of("rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY"))
78+
.flags(SponsorshipTransferFlags.builder().tfSponsorshipEnd(true).build())
79+
.build()
80+
).isInstanceOf(IllegalStateException.class)
81+
.hasMessageContaining("Sponsor must not be present when tfSponsorshipEnd is set");
82+
}
83+
84+
@Test
85+
public void endSponsorshipWithReserveFlagFails() {
86+
assertThatThrownBy(() -> SponsorshipTransfer.builder()
87+
.account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
88+
.fee(XrpCurrencyAmount.ofDrops(10))
89+
.sequence(UnsignedInteger.ONE)
90+
.objectId(Hash256.of("E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"))
91+
.sponsorFlags(SponsorFlags.SPONSOR_RESERVE)
92+
.flags(SponsorshipTransferFlags.builder().tfSponsorshipEnd(true).build())
93+
.build()
94+
).isInstanceOf(IllegalStateException.class)
95+
.hasMessageContaining("SponsorFlags must not include spfSponsorReserve when tfSponsorshipEnd is set");
96+
}
97+
98+
@Test
99+
public void endSponsorshipWithFeeFlagSucceeds() {
100+
// tfSponsorshipEnd permits SponsorFlags that do not include spfSponsorReserve (e.g. spfSponsorFee).
101+
SponsorshipTransfer transfer = SponsorshipTransfer.builder()
102+
.account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
103+
.fee(XrpCurrencyAmount.ofDrops(10))
104+
.sequence(UnsignedInteger.ONE)
105+
.objectId(Hash256.of("E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"))
106+
.sponsorFlags(SponsorFlags.SPONSOR_FEE)
107+
.flags(SponsorshipTransferFlags.builder().tfSponsorshipEnd(true).build())
108+
.build();
109+
110+
assertThat(transfer.sponsorFlags()).isPresent().get().isEqualTo(SponsorFlags.SPONSOR_FEE);
111+
}
112+
113+
@Test
114+
public void createSponsorshipWithoutSponsorFails() {
115+
assertThatThrownBy(() -> SponsorshipTransfer.builder()
116+
.account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
117+
.fee(XrpCurrencyAmount.ofDrops(10))
118+
.sequence(UnsignedInteger.ONE)
119+
.objectId(Hash256.of("E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"))
120+
.sponsorFlags(SponsorFlags.SPONSOR_RESERVE)
121+
.flags(SponsorshipTransferFlags.builder().tfSponsorshipCreate(true).build())
122+
.build()
123+
).isInstanceOf(IllegalStateException.class)
124+
.hasMessageContaining("Sponsor and SponsorFlags are both required");
125+
}
126+
127+
@Test
128+
public void createSponsorshipWithoutSponsorFlagsFails() {
129+
assertThatThrownBy(() -> SponsorshipTransfer.builder()
130+
.account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
131+
.fee(XrpCurrencyAmount.ofDrops(10))
132+
.sequence(UnsignedInteger.ONE)
133+
.objectId(Hash256.of("E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"))
134+
.sponsor(Address.of("rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY"))
135+
.flags(SponsorshipTransferFlags.builder().tfSponsorshipCreate(true).build())
136+
.build()
137+
).isInstanceOf(IllegalStateException.class)
138+
.hasMessageContaining("Sponsor and SponsorFlags are both required");
139+
}
140+
70141
@Test
71142
public void transactionType() {
72143
SponsorshipTransfer transfer = SponsorshipTransfer.builder()

0 commit comments

Comments
 (0)