From 044b4c368dad07fd1b16a96965b40cb0ffb51ccb Mon Sep 17 00:00:00 2001 From: nkramer44 Date: Wed, 10 Feb 2021 17:15:02 -0500 Subject: [PATCH] Precompute transaction hashes (#55) * add SignedTransaction to allow clients to precompute hash * move SignedTransaction to model module --- .../org/xrpl/xrpl4j/client/XrplClient.java | 266 +++++++++--------- .../org/xrpl/xrpl4j/keypairs/HashUtils.java | 4 +- .../transactions/SignedTransaction.java | 64 +++++ .../transactions/SignedTransactionTest.java | 37 +++ 4 files changed, 241 insertions(+), 130 deletions(-) create mode 100644 xrpl4j-model/src/main/java/org/xrpl/xrpl4j/model/client/transactions/SignedTransaction.java create mode 100644 xrpl4j-model/src/test/java/org/xrpl/xrpl4j/model/client/transactions/SignedTransactionTest.java diff --git a/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java b/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java index 0a6bb55af..b084306fc 100644 --- a/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java +++ b/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java @@ -23,7 +23,6 @@ import org.xrpl.xrpl4j.model.client.accounts.AccountTransactionsResult; import org.xrpl.xrpl4j.model.client.channels.ChannelVerifyRequestParams; import org.xrpl.xrpl4j.model.client.channels.ChannelVerifyResult; -import org.xrpl.xrpl4j.model.client.channels.ImmutableChannelVerifyRequestParams; import org.xrpl.xrpl4j.model.client.fees.FeeResult; import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams; import org.xrpl.xrpl4j.model.client.ledger.LedgerResult; @@ -31,6 +30,7 @@ import org.xrpl.xrpl4j.model.client.path.RipplePathFindResult; import org.xrpl.xrpl4j.model.client.server.ServerInfo; import org.xrpl.xrpl4j.model.client.server.ServerInfoResult; +import org.xrpl.xrpl4j.model.client.transactions.SignedTransaction; import org.xrpl.xrpl4j.model.client.transactions.SubmitMultiSignedRequestParams; import org.xrpl.xrpl4j.model.client.transactions.SubmitMultiSignedResult; import org.xrpl.xrpl4j.model.client.transactions.SubmitRequestParams; @@ -66,7 +66,7 @@ * A client which wraps a rippled network client and is responsible for higher order functionality such as signing * and serializing transactions, as well as hiding certain implementation details from the public API such as JSON * RPC request object creation. - * + *

* Note: This client is currently marked as {@link Beta}, and should be used as a reference implementation ONLY. */ @Beta @@ -92,7 +92,7 @@ public XrplClient(HttpUrl rippledUrl) { /** * Submit a {@link Transaction} to the XRP Ledger. * - * @param The type of {@link Transaction} that is being submitted. + * @param The type of {@link Transaction} that is being submitted. * @param wallet The {@link Wallet} of the XRPL account submitting {@code unsignedTransaction}. * @param unsignedTransaction An unsigned {@link Transaction} to submit. {@link Transaction#transactionSignature()} * must not be provided, and {@link Transaction#signingPublicKey()} must be provided. @@ -102,46 +102,57 @@ public XrplClient(HttpUrl rippledUrl) { * @see "https://xrpl.org/submit.html" */ public SubmitResult submit( - Wallet wallet, - T unsignedTransaction + Wallet wallet, + T unsignedTransaction ) throws JsonRpcClientErrorException { - try { - Preconditions.checkArgument( - unsignedTransaction.signingPublicKey().isPresent(), - "Transaction.signingPublicKey() must be set." - ); - - String signedTransaction = serializeAndSignTransaction(wallet, unsignedTransaction); - JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.SUBMIT) - .addParams(SubmitRequestParams.of(signedTransaction)) - .build(); - JavaType resultType = objectMapper.getTypeFactory() - .constructParametricType(SubmitResult.class, unsignedTransaction.getClass()); - return jsonRpcClient.send(request, resultType); - } catch (JsonProcessingException e) { - throw new IllegalStateException(e); - } + Preconditions.checkArgument( + unsignedTransaction.signingPublicKey().isPresent(), + "Transaction.signingPublicKey() must be set." + ); + + SignedTransaction signedTransaction = signTransaction(wallet, unsignedTransaction); + return submit(signedTransaction); + } + + /** + * Submit a {@link SignedTransaction} to the ledger. + * + * @param signedTransaction A {@link SignedTransaction} to submit. + * @param The type of {@link Transaction} contained in the {@link SignedTransaction} object. + * + * @return The {@link SubmitResult} resulting from the submission request. + * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. + */ + public SubmitResult submit( + SignedTransaction signedTransaction + ) throws JsonRpcClientErrorException { + JsonRpcRequest request = JsonRpcRequest.builder() + .method(XrplMethods.SUBMIT) + .addParams(SubmitRequestParams.of(signedTransaction.signedTransactionBlob())) + .build(); + JavaType resultType = objectMapper.getTypeFactory() + .constructParametricType(SubmitResult.class, signedTransaction.signedTransaction().getClass()); + return jsonRpcClient.send(request, resultType); } /** * Submit a multisigned {@link Transaction} to the ledger. * * @param transaction A multisigned {@link Transaction}. - * @param A type parameter for the type of {@link Transaction} being submitted. + * @param A type parameter for the type of {@link Transaction} being submitted. * * @return A {@link SubmitMultiSignedResult} of type {@link T}. * @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error. */ public SubmitMultiSignedResult submitMultisigned( - T transaction + T transaction ) throws JsonRpcClientErrorException { JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.SUBMIT_MULTISIGNED) - .addParams(SubmitMultiSignedRequestParams.of(transaction)) - .build(); + .method(XrplMethods.SUBMIT_MULTISIGNED) + .addParams(SubmitMultiSignedRequestParams.of(transaction)) + .build(); JavaType resultType = objectMapper.getTypeFactory().constructParametricType( - SubmitMultiSignedResult.class, transaction.getClass() + SubmitMultiSignedResult.class, transaction.getClass() ); return jsonRpcClient.send(request, resultType); } @@ -155,8 +166,8 @@ public SubmitMultiSignedResult submitMultisigned( */ public FeeResult fee() throws JsonRpcClientErrorException { JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.FEE) - .build(); + .method(XrplMethods.FEE) + .build(); return jsonRpcClient.send(request, FeeResult.class); } @@ -170,8 +181,8 @@ public FeeResult fee() throws JsonRpcClientErrorException { */ public ServerInfo serverInfo() throws JsonRpcClientErrorException { JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.SERVER_INFO) - .build(); + .method(XrplMethods.SERVER_INFO) + .build(); return jsonRpcClient.send(request, ServerInfoResult.class).info(); } @@ -187,9 +198,9 @@ public ServerInfo serverInfo() throws JsonRpcClientErrorException { */ public AccountChannelsResult accountChannels(AccountChannelsRequestParams params) throws JsonRpcClientErrorException { JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.ACCOUNT_CHANNELS) - .addParams(params) - .build(); + .method(XrplMethods.ACCOUNT_CHANNELS) + .addParams(params) + .build(); return jsonRpcClient.send(request, AccountChannelsResult.class); } @@ -205,9 +216,9 @@ public AccountChannelsResult accountChannels(AccountChannelsRequestParams params */ public AccountInfoResult accountInfo(AccountInfoRequestParams params) throws JsonRpcClientErrorException { JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.ACCOUNT_INFO) - .addParams(params) - .build(); + .method(XrplMethods.ACCOUNT_INFO) + .addParams(params) + .build(); return jsonRpcClient.send(request, AccountInfoResult.class); } @@ -223,9 +234,9 @@ public AccountInfoResult accountInfo(AccountInfoRequestParams params) throws Jso */ public AccountObjectsResult accountObjects(AccountObjectsRequestParams params) throws JsonRpcClientErrorException { JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.ACCOUNT_OBJECTS) - .addParams(params) - .build(); + .method(XrplMethods.ACCOUNT_OBJECTS) + .addParams(params) + .build(); return jsonRpcClient.send(request, AccountObjectsResult.class); } @@ -267,23 +278,23 @@ public AccountTransactionsResult accountTransactions(AccountTransactionsRequestP * * @param params The {@link TransactionRequestParams} to send in the request. * @param transactionType The {@link Transaction} type of the transaction with the hash {@code params.transaction()}. - * @param Type parameter for the type of {@link Transaction} that the {@link TransactionResult} will + * @param Type parameter for the type of {@link Transaction} that the {@link TransactionResult} will * contain. * * @return A {@link TransactionResult} containing the requested transaction and other metadata. * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public TransactionResult transaction( - TransactionRequestParams params, - Class transactionType + TransactionRequestParams params, + Class transactionType ) throws JsonRpcClientErrorException { JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.TX) - .addParams(params) - .build(); + .method(XrplMethods.TX) + .addParams(params) + .build(); JavaType resultType = objectMapper.getTypeFactory() - .constructParametricType(TransactionResult.class, transactionType); + .constructParametricType(TransactionResult.class, transactionType); return jsonRpcClient.send(request, resultType); } @@ -297,9 +308,9 @@ public TransactionResult transaction( */ public LedgerResult ledger(LedgerRequestParams params) throws JsonRpcClientErrorException { JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.LEDGER) - .addParams(params) - .build(); + .method(XrplMethods.LEDGER) + .addParams(params) + .build(); return jsonRpcClient.send(request, LedgerResult.class); } @@ -314,9 +325,9 @@ public LedgerResult ledger(LedgerRequestParams params) throws JsonRpcClientError */ public RipplePathFindResult ripplePathFind(RipplePathFindRequestParams params) throws JsonRpcClientErrorException { JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.RIPPLE_PATH_FIND) - .addParams(params) - .build(); + .method(XrplMethods.RIPPLE_PATH_FIND) + .addParams(params) + .build(); return jsonRpcClient.send(request, RipplePathFindResult.class); } @@ -331,9 +342,9 @@ public RipplePathFindResult ripplePathFind(RipplePathFindRequestParams params) t */ public AccountLinesResult accountLines(AccountLinesRequestParams params) throws JsonRpcClientErrorException { JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.ACCOUNT_LINES) - .addParams(params) - .build(); + .method(XrplMethods.ACCOUNT_LINES) + .addParams(params) + .build(); return jsonRpcClient.send(request, AccountLinesResult.class); } @@ -342,7 +353,7 @@ public AccountLinesResult accountLines(AccountLinesRequestParams params) throws * Verify a payment channel claim signature by making a "channel_verify" rippled API method call. * * @param channelId A {@link Hash256} containing the Channel ID. - * @param amount An {@link XrpCurrencyAmount} representing the amount of the claim. + * @param amount An {@link XrpCurrencyAmount} representing the amount of the claim. * @param signature The signature of the {@link PaymentChannelClaim} transaction. * @param publicKey A {@link String} containing the public key associated with the key used to generate the signature. * @@ -350,49 +361,48 @@ public AccountLinesResult accountLines(AccountLinesRequestParams params) throws * @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error. */ public ChannelVerifyResult channelVerify( - Hash256 channelId, - XrpCurrencyAmount amount, - String signature, - String publicKey + Hash256 channelId, + XrpCurrencyAmount amount, + String signature, + String publicKey ) throws JsonRpcClientErrorException { ChannelVerifyRequestParams params = ChannelVerifyRequestParams.builder() - .channelId(channelId) - .amount(amount) - .signature(signature) - .publicKey(publicKey) - .build(); + .channelId(channelId) + .amount(amount) + .signature(signature) + .publicKey(publicKey) + .build(); JsonRpcRequest request = JsonRpcRequest.builder() - .method(XrplMethods.CHANNEL_VERIFY) - .addParams(params) - .build(); + .method(XrplMethods.CHANNEL_VERIFY) + .addParams(params) + .build(); return jsonRpcClient.send(request, ChannelVerifyResult.class); } - /** - * Serialize a {@link Transaction} to binary and sign it using {@code wallet.privateKey()}. - * - * @param wallet The {@link Wallet} of the XRPL account submitting {@code unsignedTransaction}. - * @param unsignedTransaction An unsigned {@link Transaction} to submit. {@link Transaction#transactionSignature()} - * must not be provided, and {@link Transaction#signingPublicKey()} must be provided. - * - * @return The signed transaction as hex encoded {@link String}. - * @throws JsonProcessingException If the transaction cannot be serialized. - */ - private String serializeAndSignTransaction( - Wallet wallet, - Transaction unsignedTransaction - ) throws JsonProcessingException { - String unsignedJson = objectMapper.writeValueAsString(unsignedTransaction); - String unsignedBinaryHex = binaryCodec.encodeForSigning(unsignedJson); - String signature = keyPairService.sign(unsignedBinaryHex, wallet.privateKey() + public SignedTransaction signTransaction( + Wallet wallet, T unsignedTransaction + ) { + try { + String unsignedJson = objectMapper.writeValueAsString(unsignedTransaction); + + String unsignedBinaryHex = binaryCodec.encodeForSigning(unsignedJson); + String signature = keyPairService.sign(unsignedBinaryHex, wallet.privateKey() .orElseThrow(() -> new RuntimeException("Wallet must provide a private key to sign the transaction."))); - Transaction signedTransaction = addSignature(unsignedTransaction, signature); - String signedJson = objectMapper.writeValueAsString(signedTransaction); - return binaryCodec.encode(signedJson); + T signedTransaction = (T) addSignature(unsignedTransaction, signature); + + String signedJson = objectMapper.writeValueAsString(signedTransaction); + String signedBinary = binaryCodec.encode(signedJson); + return SignedTransaction.builder() + .signedTransaction(signedTransaction) + .signedTransactionBlob(signedBinary) + .build(); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } /** @@ -408,81 +418,81 @@ private String serializeAndSignTransaction( * @return A copy of {@code unsignedTransaction} with the {@link Transaction#transactionSignature()} field added. */ private Transaction addSignature( - Transaction unsignedTransaction, - String signature + Transaction unsignedTransaction, + String signature ) { if (Payment.class.isAssignableFrom(unsignedTransaction.getClass())) { return Payment.builder().from((Payment) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (AccountSet.class.isAssignableFrom(unsignedTransaction.getClass())) { return AccountSet.builder().from((AccountSet) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (AccountDelete.class.isAssignableFrom(unsignedTransaction.getClass())) { return AccountDelete.builder().from((AccountDelete) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (CheckCancel.class.isAssignableFrom(unsignedTransaction.getClass())) { return CheckCancel.builder().from((CheckCancel) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (CheckCash.class.isAssignableFrom(unsignedTransaction.getClass())) { return CheckCash.builder().from((CheckCash) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (CheckCreate.class.isAssignableFrom(unsignedTransaction.getClass())) { return CheckCreate.builder().from((CheckCreate) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (DepositPreAuth.class.isAssignableFrom(unsignedTransaction.getClass())) { return DepositPreAuth.builder().from((DepositPreAuth) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (EscrowCreate.class.isAssignableFrom(unsignedTransaction.getClass())) { return EscrowCreate.builder().from((EscrowCreate) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (EscrowCancel.class.isAssignableFrom(unsignedTransaction.getClass())) { return EscrowCancel.builder().from((EscrowCancel) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (EscrowFinish.class.isAssignableFrom(unsignedTransaction.getClass())) { return EscrowFinish.builder().from((EscrowFinish) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (TrustSet.class.isAssignableFrom(unsignedTransaction.getClass())) { return TrustSet.builder().from((TrustSet) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (OfferCreate.class.isAssignableFrom(unsignedTransaction.getClass())) { return OfferCreate.builder().from((OfferCreate) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (OfferCancel.class.isAssignableFrom(unsignedTransaction.getClass())) { return OfferCancel.builder().from((OfferCancel) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (PaymentChannelCreate.class.isAssignableFrom(unsignedTransaction.getClass())) { return PaymentChannelCreate.builder().from((PaymentChannelCreate) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (PaymentChannelClaim.class.isAssignableFrom(unsignedTransaction.getClass())) { return PaymentChannelClaim.builder().from((PaymentChannelClaim) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (PaymentChannelFund.class.isAssignableFrom(unsignedTransaction.getClass())) { return PaymentChannelFund.builder().from((PaymentChannelFund) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (SetRegularKey.class.isAssignableFrom(unsignedTransaction.getClass())) { return SetRegularKey.builder().from((SetRegularKey) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } else if (SignerListSet.class.isAssignableFrom(unsignedTransaction.getClass())) { return SignerListSet.builder().from((SignerListSet) unsignedTransaction) - .transactionSignature(signature) - .build(); + .transactionSignature(signature) + .build(); } // Never happens diff --git a/xrpl4j-keypairs/src/main/java/org/xrpl/xrpl4j/keypairs/HashUtils.java b/xrpl4j-keypairs/src/main/java/org/xrpl/xrpl4j/keypairs/HashUtils.java index a5f27662a..c8ee9c23b 100644 --- a/xrpl4j-keypairs/src/main/java/org/xrpl/xrpl4j/keypairs/HashUtils.java +++ b/xrpl4j-keypairs/src/main/java/org/xrpl/xrpl4j/keypairs/HashUtils.java @@ -18,7 +18,7 @@ public class HashUtils { * * @return An {@link UnsignedByteArray} containing the first half of the SHA-512 hash of bytes. */ - static UnsignedByteArray sha512Half(UnsignedByteArray bytes) { + public static UnsignedByteArray sha512Half(UnsignedByteArray bytes) { return sha512Half(bytes.toByteArray()); } @@ -29,7 +29,7 @@ static UnsignedByteArray sha512Half(UnsignedByteArray bytes) { * * @return An {@link UnsignedByteArray} containing the first half of the SHA-512 hash of bytes. */ - static UnsignedByteArray sha512Half(byte[] bytes) { + public static UnsignedByteArray sha512Half(byte[] bytes) { return UnsignedByteArray.of(copyOfRange(Hashing.sha512().hashBytes(bytes).asBytes(), 0, 32)); } diff --git a/xrpl4j-model/src/main/java/org/xrpl/xrpl4j/model/client/transactions/SignedTransaction.java b/xrpl4j-model/src/main/java/org/xrpl/xrpl4j/model/client/transactions/SignedTransaction.java new file mode 100644 index 000000000..f24314258 --- /dev/null +++ b/xrpl4j-model/src/main/java/org/xrpl/xrpl4j/model/client/transactions/SignedTransaction.java @@ -0,0 +1,64 @@ +package org.xrpl.xrpl4j.model.client.transactions; + +import static java.util.Arrays.copyOfRange; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.transactions.Hash256; +import org.xrpl.xrpl4j.model.transactions.Transaction; + +/** + * Represents a transaction that has been signed. + * + * @param The type of {@link Transaction} that was signed. + */ +@Value.Immutable +@JsonSerialize(as = ImmutableSignedTransaction.class) +@JsonDeserialize(as = ImmutableSignedTransaction.class) +public interface SignedTransaction { + + /** + * The hash prefix used by the XRPL to identify transaction hashes. + */ + String SIGNED_TRANSACTION_HASH_PREFIX = "54584E00"; + + static ImmutableSignedTransaction.Builder builder() { + return ImmutableSignedTransaction.builder(); + } + + /** + * The signed transaction. + * + * @return A transaction that has been signed. + */ + T signedTransaction(); + + /** + * The signed transaction, as a hex encoded binary string. + * + * @return A {@link String} containing the signed transaction blob. + */ + String signedTransactionBlob(); + + /** + * The hash of the signed transaction. This field is derived by computing the SHA512Half of the Signed Transaction + * hash prefix concatenated with the signed transaction blob. + * + * @return A {@link Hash256} containing the transaction hash. + */ + @SuppressWarnings("UnstableApiUsage") + @Value.Derived + default Hash256 hash() { + byte[] hashBytes = copyOfRange( + Hashing.sha512().hashBytes( + BaseEncoding.base16().decode(SIGNED_TRANSACTION_HASH_PREFIX.concat(signedTransactionBlob().toUpperCase())) + ).asBytes(), + 0, + 32 + ); + return Hash256.of(BaseEncoding.base16().encode(hashBytes)); + } +} diff --git a/xrpl4j-model/src/test/java/org/xrpl/xrpl4j/model/client/transactions/SignedTransactionTest.java b/xrpl4j-model/src/test/java/org/xrpl/xrpl4j/model/client/transactions/SignedTransactionTest.java new file mode 100644 index 000000000..de48aa2d9 --- /dev/null +++ b/xrpl4j-model/src/test/java/org/xrpl/xrpl4j/model/client/transactions/SignedTransactionTest.java @@ -0,0 +1,37 @@ +package org.xrpl.xrpl4j.model.client.transactions; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.primitives.UnsignedInteger; +import org.junit.Test; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Payment; +import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; + +public class SignedTransactionTest { + + @Test + public void computesCorrectTransactionHash() { + SignedTransaction signedTransaction = SignedTransaction.builder() + .signedTransaction( + Payment.builder() + .account(Address.of("rU6K7V3Po4snVhBBaU29sesqs2qTQJWDw1")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(4)) + .destination(Address.of("rEqrVunkmDhWNGHELTzQmn4mX7LKvdomfq")) + .amount(XrpCurrencyAmount.ofDrops(12345)) + .signingPublicKey("030D58EB48B4420B1F7B9DF55087E0E29FEF0E8468F9A6825B01CA2C361042D435") + .transactionSignature("304402207B82800C3289427D6F60421CDF88545BEFC6A7C9CED15A2C53E39994E52BCED402204" + + "43865800626F7FD02B369A875FA449E6204A46C5910E406018776CC08C948CA") + .build() + ) + .signedTransactionBlob("1200002280000000240000000461400000000000303968400000000000000A7321030D58EB48B4420B1F" + + "7B9DF55087E0E29FEF0E8468F9A6825B01CA2C361042D4357446304402207B82800C3289427D6F60421CDF88545BEFC6A7C9CED15" + + "A2C53E39994E52BCED40220443865800626F7FD02B369A875FA449E6204A46C5910E406018776CC08C948CA81148049717CC94878" + + "9F32F267ADC2582484E3DFA698831495FD80922EDD581C663FF9F8E948D0E13CBBE41C") + .build(); + + String expectedHash = "AD616E7F93DC9E5749222FCC644A95F19FB1893446A0FF47CA9B550F4D5DAB5D"; + assertThat(signedTransaction.hash().value()).isEqualTo(expectedHash); + } +}