Skip to content

Commit

Permalink
Merge branch 'main' of github.com:XRPLF/xrpl4j into releases/v4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
sappenin committed Dec 18, 2024
2 parents 05ed211 + 5013020 commit 19491ff
Show file tree
Hide file tree
Showing 26 changed files with 1,202 additions and 348 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ and address generation, transaction serialization and signing, provides useful J
- Example usage can be found in the `xrpl4j-integration-tests`
module [here](https://github.com/XRPLF/xrpl4j/tree/main/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests).

## Usage
## Usage

### Requirements

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

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

- **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)
- **xrpl4j-core**: [![javadoc](https://javadoc.io/badge2/org.xrpl/xrpl4j-core/javadoc.svg?color=blue)](https://javadoc.io/doc/org.xrpl/xrpl4j-core)
- Provides core primitives like seeds, public/private keys definitions (supports secp256k1 and ed25519 key types
and signing algorithms), signature interfaces, address and binary codecs etc. Also provides Java objects which model XRP Ledger objects,
as well as request parameters and response results for the `rippled` websocket and JSON RPC APIs.
Expand Down
20 changes: 15 additions & 5 deletions xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -287,15 +287,18 @@ protected Optional<? extends TransactionResult<? extends Transaction>> getValida
* Check if there missing ledgers in rippled in the given range.
*
* @param submittedLedgerSequence {@link LedgerIndex} at which the {@link Transaction} was submitted on.
* @param lastLedgerSequence he ledger index/sequence of type {@link UnsignedInteger} after which the transaction
* will expire and won't be applied to the ledger.
* @param lastLedgerSequence The ledger index/sequence of type {@link UnsignedInteger} after which the
* transaction will expire and won't be applied to the ledger.
*
* @return {@link Boolean} to indicate if there are gaps in the ledger range.
*/
protected boolean ledgerGapsExistBetween(
final UnsignedLong submittedLedgerSequence,
final UnsignedLong lastLedgerSequence
UnsignedLong lastLedgerSequence
) {
Objects.requireNonNull(submittedLedgerSequence);
Objects.requireNonNull(lastLedgerSequence);

final ServerInfoResult serverInfo;
try {
serverInfo = this.serverInformation();
Expand All @@ -304,6 +307,11 @@ protected boolean ledgerGapsExistBetween(
return true; // Assume ledger gaps exist so this can be retried.
}

// Ensure the lastLedgerSequence is (at least) as large as submittedLedgerSequence
if (FluentCompareTo.is(lastLedgerSequence).lessThan(submittedLedgerSequence)) {
lastLedgerSequence = submittedLedgerSequence;
}

Range<UnsignedLong> submittedToLast = Range.closed(submittedLedgerSequence, lastLedgerSequence);
return serverInfo.info().completeLedgers().stream()
.noneMatch(range -> range.encloses(submittedToLast));
Expand Down Expand Up @@ -369,8 +377,10 @@ public Finality isFinal(
LOGGER.debug("Transaction with hash: {} has not expired yet, check again", transactionHash);
return Finality.builder().finalityStatus(FinalityStatus.NOT_FINAL).build();
} else {
boolean isMissingLedgers = ledgerGapsExistBetween(UnsignedLong.valueOf(submittedOnLedgerIndex.toString()),
UnsignedLong.valueOf(lastLedgerSequence.toString()));
boolean isMissingLedgers = ledgerGapsExistBetween(
UnsignedLong.valueOf(submittedOnLedgerIndex.toString()),
UnsignedLong.valueOf(lastLedgerSequence.toString())
);
if (isMissingLedgers) {
LOGGER.debug("Transaction with hash: {} has expired and rippled is missing some to confirm if it" +
" was validated", transactionHash);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import com.ripple.cryptoconditions.CryptoConditionReader;
import com.ripple.cryptoconditions.CryptoConditionWriter;
import com.ripple.cryptoconditions.Fulfillment;
import com.ripple.cryptoconditions.PreimageSha256Fulfillment;
import com.ripple.cryptoconditions.PreimageSha256Fulfillment.AbstractPreimageSha256Fulfillment;
import com.ripple.cryptoconditions.der.DerEncodingException;
import org.immutables.value.Value;
import org.slf4j.Logger;
Expand All @@ -41,6 +43,7 @@
import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag;

import java.util.Arrays;
import java.util.Base64;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -69,25 +72,39 @@ static ImmutableEscrowFinish.Builder builder() {
* transaction increases if it contains a fulfillment. If the transaction contains a fulfillment, the transaction cost
* is 330 drops of XRP plus another 10 drops for every 16 bytes in size of the preimage.
*
* @param currentLedgerFeeDrops The number of drops that the ledger demands at present.
* @param fulfillment The {@link Fulfillment} that is being presented to the ledger for computation
* purposes.
* @param currentLedgerBaseFeeDrops The number of drops that the ledger demands at present.
* @param fulfillment The {@link Fulfillment} that is being presented to the ledger for computation
* purposes.
*
* @return An {@link XrpCurrencyAmount} representing the computed fee.
*
* @see "https://xrpl.org/escrowfinish.html"
*/
static XrpCurrencyAmount computeFee(final XrpCurrencyAmount currentLedgerFeeDrops, final Fulfillment fulfillment) {
Objects.requireNonNull(currentLedgerFeeDrops);
static XrpCurrencyAmount computeFee(
final XrpCurrencyAmount currentLedgerBaseFeeDrops,
final Fulfillment<?> fulfillment
) {
Objects.requireNonNull(currentLedgerBaseFeeDrops);
Objects.requireNonNull(fulfillment);

UnsignedLong newFee =
currentLedgerFeeDrops.value() // <-- usually 10 drops, per the docs.
// <-- https://github.com/ripple/rippled/blob/develop/src/ripple/app/tx/impl/Escrow.cpp#L362
.plus(UnsignedLong.valueOf(320))
// <-- 10 drops for each additional 16 bytes.
.plus(UnsignedLong.valueOf(10 * (fulfillment.getDerivedCondition().getCost() / 16)));
return XrpCurrencyAmount.of(newFee);
if (PreimageSha256Fulfillment.class.isAssignableFrom(fulfillment.getClass())) {

final long fulfillmentByteSize = Base64.getUrlDecoder().decode(
((PreimageSha256Fulfillment) fulfillment).getEncodedPreimage()
).length;
// See https://xrpl.org/docs/references/protocol/transactions/types/escrowfinish#escrowfinish-fields for
// computing the additional fee for Escrows.
// In particular: `extraFee = view.fees().base * (32 + (fb->size() / 16))`
// See https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Escrow.cpp#L368
final long baseFee = currentLedgerBaseFeeDrops.value().longValue();
final long extraFeeDrops = baseFee * (32 + (fulfillmentByteSize / 16));
final long totalFeeDrops = baseFee + extraFeeDrops; // <-- Add an extra base fee
return XrpCurrencyAmount.of(
UnsignedLong.valueOf(totalFeeDrops)
);
} else {
throw new RuntimeException("Only PreimageSha256Fulfillment is supported.");
}
}

/**
Expand Down Expand Up @@ -144,11 +161,11 @@ default TransactionFlags flags() {
*
* <p>Note that a similar field does not exist on {@link EscrowCreate},
* {@link org.xrpl.xrpl4j.model.ledger.EscrowObject}, or
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} because {@link EscrowCreate}s with
* malformed conditions will never be included in a ledger by the XRPL. Because of this fact, an
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} because {@link EscrowCreate}s with malformed
* conditions will never be included in a ledger by the XRPL. Because of this fact, an
* {@link org.xrpl.xrpl4j.model.ledger.EscrowObject} and
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} will also never contain a malformed
* crypto condition.</p>
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} will also never contain a malformed crypto
* condition.</p>
*
* @return An {@link Optional} {@link String} containing the hex-encoded PREIMAGE-SHA-256 condition.
*/
Expand Down Expand Up @@ -191,8 +208,8 @@ default TransactionFlags flags() {
* <p>If {@link #condition()} is present but {@link #conditionRawValue()} is empty, we set
* {@link #conditionRawValue()} to the underlying value of {@link #condition()}.</p>
* <p>If {@link #condition()} is empty and {@link #conditionRawValue()} is present, we will set
* {@link #condition()} to the {@link Condition} representing the raw condition value, or leave
* {@link #condition()} empty if {@link #conditionRawValue()} is a malformed {@link Condition}.</p>
* {@link #condition()} to the {@link Condition} representing the raw condition value, or leave {@link #condition()}
* empty if {@link #conditionRawValue()} is a malformed {@link Condition}.</p>
*
* @return A normalized {@link EscrowFinish}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,28 @@
import org.xrpl.xrpl4j.model.flags.TransactionFlags;
import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory;
import org.xrpl.xrpl4j.model.transactions.Address;
import org.xrpl.xrpl4j.model.transactions.Memo;
import org.xrpl.xrpl4j.model.transactions.MemoWrapper;
import org.xrpl.xrpl4j.model.transactions.Payment;
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;

import java.util.Arrays;
import java.util.Collections;

/**
* Unit tests for {@link SingleSignedTransaction}.
*/
class SignedTransactionTest {

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

final Signature signature = Signature.fromBase16(
"304502210093257D8E88D2A92CE55977641F72CCD235AB76B1AE189BE3377F30A69B131C49" +
"02200B79836114069F0D331418D05818908D85DE755AE5C2DDF42E9637FE1C11754F"
);

final Payment signedPayment = Payment.builder().from(unsignedTransaction)
.transactionSignature(Signature.fromBase16(
"304502210093257D8E88D2A92CE55977641F72CCD235AB76B1AE189BE3377F30A6" +
"9B131C4902200B79836114069F0D331418D05818908D85DE755AE5C2DDF42E9637FE1C11754F"
.transactionSignature(signature)
.build();

SingleSignedTransaction<Payment> signedTransaction = SingleSignedTransaction.<Payment>builder()
.signedTransaction(signedPayment)
.signature(signature)
.unsignedTransaction(unsignedTransaction)
.build();

String expectedHash = "F847C96B2EEB0609F16C9DB9D74A0CB123B5EAF5B626207977335BF0A1EF53C3";
assertThat(signedTransaction.hash().value()).isEqualTo(expectedHash);
assertThat(signedTransaction.unsignedTransaction()).isEqualTo(unsignedTransaction);
assertThat(signedTransaction.signedTransaction()).isEqualTo(signedPayment);
assertThat(signedTransaction.signedTransactionBytes().hexValue()).isEqualTo(
XrplBinaryCodec.getInstance().encode(ObjectMapperFactory.create().writeValueAsString(signedPayment))
);
}

/**
* This test constructs the transaction with hash 1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997 to
* check that the hash that's on livenet matches what this library computes.
*
* @see "https://livenet.xrpl.org/transactions/1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997"
*/
@Test
public void computesCorrectTransactionHashWithUnsetFlags() throws JsonProcessingException {
final Payment unsignedTransaction = Payment.builder()
.account(Address.of("rGWx7VAsnwVKRbPFPpvy8Lo4nFf5xjj6Zb"))
.amount(XrpCurrencyAmount.ofDrops(1))
.destination(Address.of("rxRpSNb1VktvzBz8JF2oJC6qaww6RZ7Lw"))
.fee(XrpCurrencyAmount.ofDrops(12))
.flags(PaymentFlags.of(TransactionFlags.UNSET.getValue())) // 0
.lastLedgerSequence(UnsignedInteger.valueOf(86481544))
.memos(Collections.singletonList(
MemoWrapper.builder()
.memo(Memo.builder()
.memoData("7B226F70223A226D696E74222C22616D6F756E74223A22313030303030303030222C22677061223A2230227D")
.build())
.build()
))
.sequence(UnsignedInteger.valueOf(84987644))
.signingPublicKey(
PublicKey.fromBase16EncodedPublicKey("ED05DC98B76FCD734BD44CDF153C34F79728485D2F24F9381CF7A284223EA258CE")
)
.build();

final Signature signature = Signature.builder().value(
UnsignedByteArray.of(BaseEncoding.base16()
.decode("304502210093257D8E88D2A92CE55977641F72CCD235AB76B1AE189BE3377F30A69B131C49" +
"02200B79836114069F0D331418D05818908D85DE755AE5C2DDF42E9637FE1C11754F"))
).build();
final Signature signature = Signature.fromBase16(
"ED6F91CCF14EE94EB072C7671A397A313E3E5CBDAFE773BB6B2F07A0E75A7E65F84B5516268DAEE12902265256" +
"EA1EF046B200148E14FF4E720C06519FD7F40F"
);

final Payment signedPayment = Payment.builder().from(unsignedTransaction)
.transactionSignature(signature)
.build();

SingleSignedTransaction<Payment> signedTransaction = SingleSignedTransaction.<Payment>builder()
.signedTransaction(signedPayment)
.signature(signature)
.unsignedTransaction(unsignedTransaction)
.build();

String expectedHash = "F847C96B2EEB0609F16C9DB9D74A0CB123B5EAF5B626207977335BF0A1EF53C3";
String expectedHash = "1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997";
assertThat(signedTransaction.hash().value()).isEqualTo(expectedHash);
assertThat(signedTransaction.unsignedTransaction()).isEqualTo(unsignedTransaction);
assertThat(signedTransaction.signedTransaction()).isEqualTo(signedPayment);
assertThat(signedTransaction.signedTransactionBytes().hexValue()).isEqualTo(
XrplBinaryCodec.getInstance().encode(ObjectMapperFactory.create().writeValueAsString(signedPayment))
);
}

/**
* This test constructs the transaction with hash 1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997 to
* check that the hash that's on livenet _does not_ match when the signature is supplied incorrectly (i.e., this test
* validates that a transaction's signature is always used to compute a transaction hash).
*
* @see "https://livenet.xrpl.org/transactions/1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997"
*/
@Test
public void computesIncorrectTransactionHashWithoutSignature() throws JsonProcessingException {
final Payment unsignedTransaction = Payment.builder()
.account(Address.of("rGWx7VAsnwVKRbPFPpvy8Lo4nFf5xjj6Zb"))
.amount(XrpCurrencyAmount.ofDrops(1))
.destination(Address.of("rxRpSNb1VktvzBz8JF2oJC6qaww6RZ7Lw"))
.fee(XrpCurrencyAmount.ofDrops(12))
.flags(PaymentFlags.of(TransactionFlags.UNSET.getValue())) // 0
.lastLedgerSequence(UnsignedInteger.valueOf(86481544))
.memos(Collections.singletonList(
MemoWrapper.builder()
.memo(Memo.builder()
.memoData("7B226F70223A226D696E74222C22616D6F756E74223A22313030303030303030222C22677061223A2230227D")
.build())
.build()
))
.sequence(UnsignedInteger.valueOf(84987644))
.signingPublicKey(
PublicKey.fromBase16EncodedPublicKey("ED05DC98B76FCD734BD44CDF153C34F79728485D2F24F9381CF7A284223EA258CE")
)
.build();

final Signature emptySignature = Signature.fromBase16("");

final Payment signedPayment = Payment.builder().from(unsignedTransaction)
.transactionSignature(emptySignature)
.build();

SingleSignedTransaction<Payment> signedTransaction = SingleSignedTransaction.<Payment>builder()
.signedTransaction(signedPayment)
.signature(emptySignature)
.unsignedTransaction(unsignedTransaction)
.build();

String expectedHash = "1A1953AC3BA3123254AA912CE507514A6AAD05EED8981A870B45F604936F0997";
assertThat(signedTransaction.hash().value()).isNotEqualTo(expectedHash);
assertThat(signedTransaction.hash().value()).isEqualTo(
"8E0EDE65ECE8A03ABDD7926B994B2F6F14514FDBD46714F4F511143A1F01A6D0"
);
assertThat(signedTransaction.unsignedTransaction()).isEqualTo(unsignedTransaction);
assertThat(signedTransaction.signedTransaction()).isEqualTo(signedPayment);
assertThat(signedTransaction.signedTransactionBytes().hexValue()).isEqualTo(
XrplBinaryCodec.getInstance().encode(ObjectMapperFactory.create().writeValueAsString(signedPayment))
);
}
}
Loading

0 comments on commit 19491ff

Please sign in to comment.