Skip to content

Commit 19491ff

Browse files
committed
Merge branch 'main' of github.com:XRPLF/xrpl4j into releases/v4.0
2 parents 05ed211 + 5013020 commit 19491ff

File tree

26 files changed

+1202
-348
lines changed

26 files changed

+1202
-348
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ and address generation, transaction serialization and signing, provides useful J
1515
- Example usage can be found in the `xrpl4j-integration-tests`
1616
module [here](https://github.com/XRPLF/xrpl4j/tree/main/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests).
1717

18-
## Usage
18+
## Usage
1919

2020
### Requirements
2121

@@ -225,7 +225,7 @@ canonical JSON encoding). Read more about each here:
225225

226226
Xrpl4j is structured as a Maven multi-module project, with the following modules:
227227

228-
- **xrpl4j-core**: [![javadoc](https://javadoc.io/badge2/org.xrpl/xrpl4j-binary-codec/javadoc.svg?color=blue)](https://javadoc.io/doc/org.xrpl/xrpl4j-binary-codec)
228+
- **xrpl4j-core**: [![javadoc](https://javadoc.io/badge2/org.xrpl/xrpl4j-core/javadoc.svg?color=blue)](https://javadoc.io/doc/org.xrpl/xrpl4j-core)
229229
- Provides core primitives like seeds, public/private keys definitions (supports secp256k1 and ed25519 key types
230230
and signing algorithms), signature interfaces, address and binary codecs etc. Also provides Java objects which model XRP Ledger objects,
231231
as well as request parameters and response results for the `rippled` websocket and JSON RPC APIs.

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -287,15 +287,18 @@ protected Optional<? extends TransactionResult<? extends Transaction>> getValida
287287
* Check if there missing ledgers in rippled in the given range.
288288
*
289289
* @param submittedLedgerSequence {@link LedgerIndex} at which the {@link Transaction} was submitted on.
290-
* @param lastLedgerSequence he ledger index/sequence of type {@link UnsignedInteger} after which the transaction
291-
* will expire and won't be applied to the ledger.
290+
* @param lastLedgerSequence The ledger index/sequence of type {@link UnsignedInteger} after which the
291+
* transaction will expire and won't be applied to the ledger.
292292
*
293293
* @return {@link Boolean} to indicate if there are gaps in the ledger range.
294294
*/
295295
protected boolean ledgerGapsExistBetween(
296296
final UnsignedLong submittedLedgerSequence,
297-
final UnsignedLong lastLedgerSequence
297+
UnsignedLong lastLedgerSequence
298298
) {
299+
Objects.requireNonNull(submittedLedgerSequence);
300+
Objects.requireNonNull(lastLedgerSequence);
301+
299302
final ServerInfoResult serverInfo;
300303
try {
301304
serverInfo = this.serverInformation();
@@ -304,6 +307,11 @@ protected boolean ledgerGapsExistBetween(
304307
return true; // Assume ledger gaps exist so this can be retried.
305308
}
306309

310+
// Ensure the lastLedgerSequence is (at least) as large as submittedLedgerSequence
311+
if (FluentCompareTo.is(lastLedgerSequence).lessThan(submittedLedgerSequence)) {
312+
lastLedgerSequence = submittedLedgerSequence;
313+
}
314+
307315
Range<UnsignedLong> submittedToLast = Range.closed(submittedLedgerSequence, lastLedgerSequence);
308316
return serverInfo.info().completeLedgers().stream()
309317
.noneMatch(range -> range.encloses(submittedToLast));
@@ -369,8 +377,10 @@ public Finality isFinal(
369377
LOGGER.debug("Transaction with hash: {} has not expired yet, check again", transactionHash);
370378
return Finality.builder().finalityStatus(FinalityStatus.NOT_FINAL).build();
371379
} else {
372-
boolean isMissingLedgers = ledgerGapsExistBetween(UnsignedLong.valueOf(submittedOnLedgerIndex.toString()),
373-
UnsignedLong.valueOf(lastLedgerSequence.toString()));
380+
boolean isMissingLedgers = ledgerGapsExistBetween(
381+
UnsignedLong.valueOf(submittedOnLedgerIndex.toString()),
382+
UnsignedLong.valueOf(lastLedgerSequence.toString())
383+
);
374384
if (isMissingLedgers) {
375385
LOGGER.debug("Transaction with hash: {} has expired and rippled is missing some to confirm if it" +
376386
" was validated", transactionHash);

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/EscrowFinish.java

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import com.ripple.cryptoconditions.CryptoConditionReader;
3333
import com.ripple.cryptoconditions.CryptoConditionWriter;
3434
import com.ripple.cryptoconditions.Fulfillment;
35+
import com.ripple.cryptoconditions.PreimageSha256Fulfillment;
36+
import com.ripple.cryptoconditions.PreimageSha256Fulfillment.AbstractPreimageSha256Fulfillment;
3537
import com.ripple.cryptoconditions.der.DerEncodingException;
3638
import org.immutables.value.Value;
3739
import org.slf4j.Logger;
@@ -41,6 +43,7 @@
4143
import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag;
4244

4345
import java.util.Arrays;
46+
import java.util.Base64;
4447
import java.util.Locale;
4548
import java.util.Objects;
4649
import java.util.Optional;
@@ -69,25 +72,39 @@ static ImmutableEscrowFinish.Builder builder() {
6972
* transaction increases if it contains a fulfillment. If the transaction contains a fulfillment, the transaction cost
7073
* is 330 drops of XRP plus another 10 drops for every 16 bytes in size of the preimage.
7174
*
72-
* @param currentLedgerFeeDrops The number of drops that the ledger demands at present.
73-
* @param fulfillment The {@link Fulfillment} that is being presented to the ledger for computation
74-
* purposes.
75+
* @param currentLedgerBaseFeeDrops The number of drops that the ledger demands at present.
76+
* @param fulfillment The {@link Fulfillment} that is being presented to the ledger for computation
77+
* purposes.
7578
*
7679
* @return An {@link XrpCurrencyAmount} representing the computed fee.
7780
*
7881
* @see "https://xrpl.org/escrowfinish.html"
7982
*/
80-
static XrpCurrencyAmount computeFee(final XrpCurrencyAmount currentLedgerFeeDrops, final Fulfillment fulfillment) {
81-
Objects.requireNonNull(currentLedgerFeeDrops);
83+
static XrpCurrencyAmount computeFee(
84+
final XrpCurrencyAmount currentLedgerBaseFeeDrops,
85+
final Fulfillment<?> fulfillment
86+
) {
87+
Objects.requireNonNull(currentLedgerBaseFeeDrops);
8288
Objects.requireNonNull(fulfillment);
8389

84-
UnsignedLong newFee =
85-
currentLedgerFeeDrops.value() // <-- usually 10 drops, per the docs.
86-
// <-- https://github.com/ripple/rippled/blob/develop/src/ripple/app/tx/impl/Escrow.cpp#L362
87-
.plus(UnsignedLong.valueOf(320))
88-
// <-- 10 drops for each additional 16 bytes.
89-
.plus(UnsignedLong.valueOf(10 * (fulfillment.getDerivedCondition().getCost() / 16)));
90-
return XrpCurrencyAmount.of(newFee);
90+
if (PreimageSha256Fulfillment.class.isAssignableFrom(fulfillment.getClass())) {
91+
92+
final long fulfillmentByteSize = Base64.getUrlDecoder().decode(
93+
((PreimageSha256Fulfillment) fulfillment).getEncodedPreimage()
94+
).length;
95+
// See https://xrpl.org/docs/references/protocol/transactions/types/escrowfinish#escrowfinish-fields for
96+
// computing the additional fee for Escrows.
97+
// In particular: `extraFee = view.fees().base * (32 + (fb->size() / 16))`
98+
// See https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Escrow.cpp#L368
99+
final long baseFee = currentLedgerBaseFeeDrops.value().longValue();
100+
final long extraFeeDrops = baseFee * (32 + (fulfillmentByteSize / 16));
101+
final long totalFeeDrops = baseFee + extraFeeDrops; // <-- Add an extra base fee
102+
return XrpCurrencyAmount.of(
103+
UnsignedLong.valueOf(totalFeeDrops)
104+
);
105+
} else {
106+
throw new RuntimeException("Only PreimageSha256Fulfillment is supported.");
107+
}
91108
}
92109

93110
/**
@@ -144,11 +161,11 @@ default TransactionFlags flags() {
144161
*
145162
* <p>Note that a similar field does not exist on {@link EscrowCreate},
146163
* {@link org.xrpl.xrpl4j.model.ledger.EscrowObject}, or
147-
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} because {@link EscrowCreate}s with
148-
* malformed conditions will never be included in a ledger by the XRPL. Because of this fact, an
164+
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} because {@link EscrowCreate}s with malformed
165+
* conditions will never be included in a ledger by the XRPL. Because of this fact, an
149166
* {@link org.xrpl.xrpl4j.model.ledger.EscrowObject} and
150-
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} will also never contain a malformed
151-
* crypto condition.</p>
167+
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} will also never contain a malformed crypto
168+
* condition.</p>
152169
*
153170
* @return An {@link Optional} {@link String} containing the hex-encoded PREIMAGE-SHA-256 condition.
154171
*/
@@ -191,8 +208,8 @@ default TransactionFlags flags() {
191208
* <p>If {@link #condition()} is present but {@link #conditionRawValue()} is empty, we set
192209
* {@link #conditionRawValue()} to the underlying value of {@link #condition()}.</p>
193210
* <p>If {@link #condition()} is empty and {@link #conditionRawValue()} is present, we will set
194-
* {@link #condition()} to the {@link Condition} representing the raw condition value, or leave
195-
* {@link #condition()} empty if {@link #conditionRawValue()} is a malformed {@link Condition}.</p>
211+
* {@link #condition()} to the {@link Condition} representing the raw condition value, or leave {@link #condition()}
212+
* empty if {@link #conditionRawValue()} is a malformed {@link Condition}.</p>
196213
*
197214
* @return A normalized {@link EscrowFinish}.
198215
*/

xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignedTransactionTest.java

Lines changed: 123 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,28 @@
3333
import org.xrpl.xrpl4j.model.flags.TransactionFlags;
3434
import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory;
3535
import org.xrpl.xrpl4j.model.transactions.Address;
36+
import org.xrpl.xrpl4j.model.transactions.Memo;
37+
import org.xrpl.xrpl4j.model.transactions.MemoWrapper;
3638
import org.xrpl.xrpl4j.model.transactions.Payment;
3739
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;
3840

41+
import java.util.Arrays;
42+
import java.util.Collections;
43+
3944
/**
4045
* Unit tests for {@link SingleSignedTransaction}.
4146
*/
4247
class SignedTransactionTest {
4348

4449
/**
45-
* This test constructs the transaction found here:
46-
* https://livenet.xrpl.org/transactions/A7AE53FE15B02E6E2F3C610FB4BA30B12392EB110F1D5E8C20880555E8639B05 to check
47-
* that the hash that's on livenet matches what this library computes. The hash you see in this test is different than
48-
* the hash found on livenet because the real transaction did not set any flags on the transaction and {@link Payment}
49-
* requires a flags field (Even if you set flags to 0, it affects the hash). However, we made {@link Payment#flags()}
50-
* nullable during development and verified that the hashes match, so we are confident that our hash calculation is
51-
* accurate.
50+
* This test constructs the transaction with hash A7AE53FE15B02E6E2F3C610FB4BA30B12392EB110F1D5E8C20880555E8639B05 to
51+
* check that the hash that's on livenet matches what this library computes. The hash you see in this test is
52+
* different from the hash found on livenet because the real transaction did not set any flags on the transaction and
53+
* {@link Payment} requires a flags field (Even if you set flags to 0, it affects the hash). However, we made
54+
* {@link Payment#flags()} nullable during development and verified that the hashes match, so we are confident that
55+
* our hash calculation is accurate.
56+
*
57+
* @see "https://livenet.xrpl.org/transactions/A7AE53FE15B02E6E2F3C610FB4BA30B12392EB110F1D5E8C20880555E8639B05"
5258
*/
5359
@Test
5460
public void computesCorrectTransactionHash() throws JsonProcessingException {
@@ -65,31 +71,132 @@ public void computesCorrectTransactionHash() throws JsonProcessingException {
6571
.destinationTag(UnsignedInteger.valueOf(371969))
6672
.build();
6773

74+
final Signature signature = Signature.fromBase16(
75+
"304502210093257D8E88D2A92CE55977641F72CCD235AB76B1AE189BE3377F30A69B131C49" +
76+
"02200B79836114069F0D331418D05818908D85DE755AE5C2DDF42E9637FE1C11754F"
77+
);
78+
6879
final Payment signedPayment = Payment.builder().from(unsignedTransaction)
69-
.transactionSignature(Signature.fromBase16(
70-
"304502210093257D8E88D2A92CE55977641F72CCD235AB76B1AE189BE3377F30A6" +
71-
"9B131C4902200B79836114069F0D331418D05818908D85DE755AE5C2DDF42E9637FE1C11754F"
80+
.transactionSignature(signature)
81+
.build();
82+
83+
SingleSignedTransaction<Payment> signedTransaction = SingleSignedTransaction.<Payment>builder()
84+
.signedTransaction(signedPayment)
85+
.signature(signature)
86+
.unsignedTransaction(unsignedTransaction)
87+
.build();
88+
89+
String expectedHash = "F847C96B2EEB0609F16C9DB9D74A0CB123B5EAF5B626207977335BF0A1EF53C3";
90+
assertThat(signedTransaction.hash().value()).isEqualTo(expectedHash);
91+
assertThat(signedTransaction.unsignedTransaction()).isEqualTo(unsignedTransaction);
92+
assertThat(signedTransaction.signedTransaction()).isEqualTo(signedPayment);
93+
assertThat(signedTransaction.signedTransactionBytes().hexValue()).isEqualTo(
94+
XrplBinaryCodec.getInstance().encode(ObjectMapperFactory.create().writeValueAsString(signedPayment))
95+
);
96+
}
97+
98+
/**
99+
* This test constructs the transaction with hash 1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997 to
100+
* check that the hash that's on livenet matches what this library computes.
101+
*
102+
* @see "https://livenet.xrpl.org/transactions/1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997"
103+
*/
104+
@Test
105+
public void computesCorrectTransactionHashWithUnsetFlags() throws JsonProcessingException {
106+
final Payment unsignedTransaction = Payment.builder()
107+
.account(Address.of("rGWx7VAsnwVKRbPFPpvy8Lo4nFf5xjj6Zb"))
108+
.amount(XrpCurrencyAmount.ofDrops(1))
109+
.destination(Address.of("rxRpSNb1VktvzBz8JF2oJC6qaww6RZ7Lw"))
110+
.fee(XrpCurrencyAmount.ofDrops(12))
111+
.flags(PaymentFlags.of(TransactionFlags.UNSET.getValue())) // 0
112+
.lastLedgerSequence(UnsignedInteger.valueOf(86481544))
113+
.memos(Collections.singletonList(
114+
MemoWrapper.builder()
115+
.memo(Memo.builder()
116+
.memoData("7B226F70223A226D696E74222C22616D6F756E74223A22313030303030303030222C22677061223A2230227D")
117+
.build())
118+
.build()
72119
))
120+
.sequence(UnsignedInteger.valueOf(84987644))
121+
.signingPublicKey(
122+
PublicKey.fromBase16EncodedPublicKey("ED05DC98B76FCD734BD44CDF153C34F79728485D2F24F9381CF7A284223EA258CE")
123+
)
73124
.build();
74125

75-
final Signature signature = Signature.builder().value(
76-
UnsignedByteArray.of(BaseEncoding.base16()
77-
.decode("304502210093257D8E88D2A92CE55977641F72CCD235AB76B1AE189BE3377F30A69B131C49" +
78-
"02200B79836114069F0D331418D05818908D85DE755AE5C2DDF42E9637FE1C11754F"))
79-
).build();
126+
final Signature signature = Signature.fromBase16(
127+
"ED6F91CCF14EE94EB072C7671A397A313E3E5CBDAFE773BB6B2F07A0E75A7E65F84B5516268DAEE12902265256" +
128+
"EA1EF046B200148E14FF4E720C06519FD7F40F"
129+
);
130+
131+
final Payment signedPayment = Payment.builder().from(unsignedTransaction)
132+
.transactionSignature(signature)
133+
.build();
80134

81135
SingleSignedTransaction<Payment> signedTransaction = SingleSignedTransaction.<Payment>builder()
82136
.signedTransaction(signedPayment)
83137
.signature(signature)
84138
.unsignedTransaction(unsignedTransaction)
85139
.build();
86140

87-
String expectedHash = "F847C96B2EEB0609F16C9DB9D74A0CB123B5EAF5B626207977335BF0A1EF53C3";
141+
String expectedHash = "1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997";
88142
assertThat(signedTransaction.hash().value()).isEqualTo(expectedHash);
89143
assertThat(signedTransaction.unsignedTransaction()).isEqualTo(unsignedTransaction);
90144
assertThat(signedTransaction.signedTransaction()).isEqualTo(signedPayment);
91145
assertThat(signedTransaction.signedTransactionBytes().hexValue()).isEqualTo(
92146
XrplBinaryCodec.getInstance().encode(ObjectMapperFactory.create().writeValueAsString(signedPayment))
93147
);
94148
}
149+
150+
/**
151+
* This test constructs the transaction with hash 1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997 to
152+
* check that the hash that's on livenet _does not_ match when the signature is supplied incorrectly (i.e., this test
153+
* validates that a transaction's signature is always used to compute a transaction hash).
154+
*
155+
* @see "https://livenet.xrpl.org/transactions/1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997"
156+
*/
157+
@Test
158+
public void computesIncorrectTransactionHashWithoutSignature() throws JsonProcessingException {
159+
final Payment unsignedTransaction = Payment.builder()
160+
.account(Address.of("rGWx7VAsnwVKRbPFPpvy8Lo4nFf5xjj6Zb"))
161+
.amount(XrpCurrencyAmount.ofDrops(1))
162+
.destination(Address.of("rxRpSNb1VktvzBz8JF2oJC6qaww6RZ7Lw"))
163+
.fee(XrpCurrencyAmount.ofDrops(12))
164+
.flags(PaymentFlags.of(TransactionFlags.UNSET.getValue())) // 0
165+
.lastLedgerSequence(UnsignedInteger.valueOf(86481544))
166+
.memos(Collections.singletonList(
167+
MemoWrapper.builder()
168+
.memo(Memo.builder()
169+
.memoData("7B226F70223A226D696E74222C22616D6F756E74223A22313030303030303030222C22677061223A2230227D")
170+
.build())
171+
.build()
172+
))
173+
.sequence(UnsignedInteger.valueOf(84987644))
174+
.signingPublicKey(
175+
PublicKey.fromBase16EncodedPublicKey("ED05DC98B76FCD734BD44CDF153C34F79728485D2F24F9381CF7A284223EA258CE")
176+
)
177+
.build();
178+
179+
final Signature emptySignature = Signature.fromBase16("");
180+
181+
final Payment signedPayment = Payment.builder().from(unsignedTransaction)
182+
.transactionSignature(emptySignature)
183+
.build();
184+
185+
SingleSignedTransaction<Payment> signedTransaction = SingleSignedTransaction.<Payment>builder()
186+
.signedTransaction(signedPayment)
187+
.signature(emptySignature)
188+
.unsignedTransaction(unsignedTransaction)
189+
.build();
190+
191+
String expectedHash = "1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997";
192+
assertThat(signedTransaction.hash().value()).isNotEqualTo(expectedHash);
193+
assertThat(signedTransaction.hash().value()).isEqualTo(
194+
"8E0EDE65ECE8A03ABDD7926B994B2F6F14514FDBD46714F4F511143A1F01A6D0"
195+
);
196+
assertThat(signedTransaction.unsignedTransaction()).isEqualTo(unsignedTransaction);
197+
assertThat(signedTransaction.signedTransaction()).isEqualTo(signedPayment);
198+
assertThat(signedTransaction.signedTransactionBytes().hexValue()).isEqualTo(
199+
XrplBinaryCodec.getInstance().encode(ObjectMapperFactory.create().writeValueAsString(signedPayment))
200+
);
201+
}
95202
}

0 commit comments

Comments
 (0)