From 1bf2c8cc99a675e5382e6fa3091575a8ac3b6690 Mon Sep 17 00:00:00 2001 From: rsteimen Date: Sun, 4 Feb 2024 10:10:55 +0100 Subject: [PATCH 1/2] Add support for unknown TransactionType (#521) --- .../model/transactions/Transaction.java | 1 + .../model/transactions/TransactionType.java | 11 +- .../transactions/UnknownTransaction.java | 236 ++++++++++++++++++ .../binary/BinarySerializationTests.java | 59 +++++ .../transactions/TransactionTypeTests.java | 7 +- 5 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/UnknownTransaction.java diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java index 40c3e7695..e7a4d225b 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java @@ -88,6 +88,7 @@ public interface Transaction { .put(ImmutableXChainModifyBridge.class, TransactionType.XCHAIN_MODIFY_BRIDGE) .put(ImmutableDidSet.class, TransactionType.DID_SET) .put(ImmutableDidDelete.class, TransactionType.DID_DELETE) + .put(UnknownTransaction.class, TransactionType.UNKNOWN) .build(); /** diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java index 244e00544..5df425b94 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java @@ -318,7 +318,12 @@ public enum TransactionType { * is subject to change.

*/ @Beta - DID_DELETE("DIDDelete"); + DID_DELETE("DIDDelete"), + + /** + * The {@link TransactionType} for an unknown transaction. + */ + UNKNOWN(""); private final String value; @@ -331,7 +336,7 @@ public enum TransactionType { * * @param value The {@link String} value corresponding to a {@link TransactionType}. * - * @return The {@link TransactionType} with the corresponding value. + * @return The {@link TransactionType} with the corresponding value or {@link TransactionType#UNKNOWN} if the given string value is unknown. */ public static TransactionType forValue(String value) { for (TransactionType transactionType : TransactionType.values()) { @@ -340,7 +345,7 @@ public static TransactionType forValue(String value) { } } - throw new IllegalArgumentException("No matching TransactionType enum value for String value " + value); + return TransactionType.UNKNOWN; } /** diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/UnknownTransaction.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/UnknownTransaction.java new file mode 100644 index 000000000..a3fe0e37a --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/UnknownTransaction.java @@ -0,0 +1,236 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.primitives.UnsignedInteger; +import com.google.common.primitives.UnsignedLong; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.crypto.signing.Signature; +import org.xrpl.xrpl4j.model.client.common.LedgerIndex; +import org.xrpl.xrpl4j.model.client.common.TimeUtils; +import org.xrpl.xrpl4j.model.flags.Flags; + +import java.time.ZonedDateTime; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class UnknownTransaction implements Transaction { + private Address account; + private String transactionTypeText; + private XrpCurrencyAmount fee; + private UnsignedInteger sequence; + private Optional ticketSequence; + private Optional accountTransactionId; + private Flags flags; + private Optional lastLedgerSequence; + private List memos; + private List signers; + private Optional sourceTag; + private Optional transactionSignature; + private Optional networkId; + private PublicKey signingPublicKey; + private Hash256 hash; + private LedgerIndex ledgerIndex; + private Optional closeDate; + + private final Map unknowns = new LinkedHashMap<>(); + + /** + * The unique {@link Address} of the account that initiated this transaction. + * + * @return The {@link Address} of the account submitting this transaction. + */ + @Override + public Address account() { + return account; + } + + /** + * The type as text of transaction. + */ + @JsonProperty("TransactionType") + public String transactionTypeText() { + return transactionTypeText; + } + + /** + * The {@link String} representation of an integer amount of XRP, in drops, to be destroyed as a cost for distributing + * this Payment transaction to the network. + * + * @return An {@link XrpCurrencyAmount} representing the transaction cost. + */ + @Override + public XrpCurrencyAmount fee() { + return fee; + } + + /** + * The sequence number of the account submitting the {@link Transaction}. A {@link Transaction} is only valid if the + * Sequence number is exactly 1 greater than the previous transaction from the same account. + * + * @return An {@link UnsignedInteger} representing the sequence of the transaction. + */ + @JsonProperty("Sequence") + public UnsignedInteger sequence() { + return sequence; + } + + /** + * The sequence number of the {@link org.xrpl.xrpl4j.model.ledger.TicketObject} to use in place of a + * {@link #sequence()} number. If this is provided, {@link #sequence()} must be 0. Cannot be used with + * {@link #accountTransactionId()}. + * + * @return An {@link UnsignedInteger} representing the ticket sequence of the transaction. + */ + @Override + public Optional ticketSequence() { + return ticketSequence; + } + + /** + * Hash value identifying another transaction. If provided, this {@link Transaction} is only valid if the sending + * account's previously-sent transaction matches the provided hash. + * + * @return An {@link Optional} of type {@link Hash256} containing the account transaction ID. + */ + @Override + public Optional accountTransactionId() { + return accountTransactionId; + } + + /** + * A bit-map of boolean flags. + */ + @JsonProperty("Flags") + public Flags flags() { + return flags; + } + + /** + * Highest ledger index this transaction can appear in. Specifying this field places a strict upper limit on how long + * the transaction can wait to be validated or rejected. + * + * @return An {@link Optional} of type {@link UnsignedInteger} representing the last ledger sequence. + */ + @Override + public Optional lastLedgerSequence() { + return lastLedgerSequence; + } + + /** + * Additional arbitrary information used to identify this {@link Transaction}. + * + * @return A {@link List} of {@link MemoWrapper}s. + */ + @Override + public List memos() { + return memos; + } + + /** + * Array of {@link SignerWrapper}s that represent a multi-signature which authorizes this {@link Transaction}. + * + * @return A {@link List} of {@link SignerWrapper}s. + */ + @Override + public List signers() { + return signers; + } + + /** + * Arbitrary {@link UnsignedInteger} used to identify the reason for this {@link Transaction}, or a sender on whose + * behalf this {@link Transaction} is made. + * + * @return An {@link Optional} {@link UnsignedInteger} representing the source account's tag. + */ + @Override + public Optional sourceTag() { + return sourceTag; + } + + /** + * The {@link PublicKey} that corresponds to the private key used to sign this transaction. If an empty string, ie + * {@link PublicKey#MULTI_SIGN_PUBLIC_KEY}, indicates a multi-signature is present in the + * {@link Transaction#signers()} field instead. + * + * @return A {@link PublicKey} containing the public key of the account submitting the transaction, or + * {@link PublicKey#MULTI_SIGN_PUBLIC_KEY} if the transaction is multi-signed. + */ + @JsonProperty("SigningPubKey") + public PublicKey signingPublicKey() { + return signingPublicKey; + } + + /** + * The signature that verifies this transaction as originating from the account it says it is from. + * + * @return An {@link Optional} {@link String} containing the transaction signature. + */ + @Override + public Optional transactionSignature() { + return transactionSignature; + } + + @Override + public Optional networkId() { + return networkId; + } + + /** + * Unique hash for the ledger, as hexadecimal. + * + * @return A {@link Hash256} containing the ledger hash. + */ + @JsonProperty("hash") + public Hash256 hash() { + return hash; + } + + /** + * The index of the ledger that this transaction was included in. + * + * @return The {@link LedgerIndex} that this transaction was included in. + */ + @JsonProperty("ledger_index") + public LedgerIndex ledgerIndex() { + return ledgerIndex; + } + + /** + * The approximate close time (using Ripple Epoch) of the ledger containing this transaction. + * This is an undocumented field. + * + * @return An optionally-present {@link UnsignedLong}. + */ + @JsonProperty("date") + public Optional closeDate() { + return closeDate; + } + + /** + * The approximate close time in UTC offset. + * This is derived from undocumented field. + * + * @return An optionally-present {@link ZonedDateTime}. + */ + public Optional closeDateHuman() { + return closeDate().map(TimeUtils::xrplTimeToZonedDateTime); + } + + @JsonAnySetter + private void putUnknown(String key, Object value) { + unknowns.put(key, value); + } + + /** + * Map of all unknown and not mapped JSON nodes. + */ + @JsonIgnore + public Map unknowns() { + return unknowns; + } +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java index d22654441..6824e47a6 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java @@ -86,6 +86,7 @@ import org.xrpl.xrpl4j.model.transactions.SignerListSet; import org.xrpl.xrpl4j.model.transactions.Transaction; import org.xrpl.xrpl4j.model.transactions.TrustSet; +import org.xrpl.xrpl4j.model.transactions.UnknownTransaction; import org.xrpl.xrpl4j.model.transactions.XChainAccountCreateCommit; import org.xrpl.xrpl4j.model.transactions.XChainAddAccountCreateAttestation; import org.xrpl.xrpl4j.model.transactions.XChainAddClaimAttestation; @@ -100,6 +101,8 @@ import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Map; public class BinarySerializationTests { @@ -2120,6 +2123,62 @@ void serializeDidDelete() throws JsonProcessingException { assertSerializesAndDeserializes(transaction, binary); } + @Test + void deserializeUnknown() throws JsonProcessingException { + String json = "{\n" + + " \"Account\": \"rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8\",\n" + + " \"Fee\": \"10\",\n" + + " \"Sequence\": 4,\n" + + " \"TransactionType\": \"UnknownCustom\",\n" + + " \"UnknownProperty\": \"value123\",\n" + + " \"UnknownObject\": {\n" + + " \"name\": \"value0\",\n" + + " \"key1\": 1\n" + + " },\n" + + " \"UnknownArray\": [\n" + + " {\n" + + " \"UnknownDetail\": {\n" + + " \"Amount\": \"1666671963\",\n" + + " \"Destination\": \"rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"UnknownDetail\": {\n" + + " \"Amount\": \"83333166\",\n" + + " \"Destination\": \"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + + UnknownTransaction transaction = objectMapper.readValue(json, UnknownTransaction.class); + + assertThat(transaction.account().value()).isEqualTo("rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8"); + assertThat(transaction.fee()).isEqualTo(XrpCurrencyAmount.ofDrops(10)); + assertThat(transaction.sequence()).isEqualTo(UnsignedInteger.valueOf(4)); + assertThat(transaction.transactionTypeText()).isEqualTo("UnknownCustom"); + + assertThat(transaction.unknowns().size()).isEqualTo(3); + assertThat(transaction.unknowns().get("UnknownProperty")).isEqualTo("value123"); + + Map unknownObject = (Map)transaction.unknowns().get("UnknownObject"); + assertThat(unknownObject).isNotNull(); + assertThat(unknownObject.get("name")).isEqualTo("value0"); + assertThat(unknownObject.get("key1")).isEqualTo(1); + + ArrayList unknownArray = (ArrayList)transaction.unknowns().get("UnknownArray"); + assertThat(unknownArray).isNotNull(); + assertThat(unknownArray.size()).isEqualTo(2); + Map elem0 = (Map)unknownArray.get(0); + Map elem0Detail = (Map)elem0.get("UnknownDetail"); + assertThat(elem0Detail.get("Amount")).isEqualTo("1666671963"); + assertThat(elem0Detail.get("Destination")).isEqualTo("rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL"); + Map elem1 = (Map)unknownArray.get(1); + Map elem1Detail = (Map)elem1.get("UnknownDetail"); + assertThat(elem1Detail.get("Amount")).isEqualTo("83333166"); + assertThat(elem1Detail.get("Destination")).isEqualTo("r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV"); + } + private void assertSerializesAndDeserializes( T transaction, String expectedBinary diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TransactionTypeTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TransactionTypeTests.java index 77beee833..da8fef274 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TransactionTypeTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TransactionTypeTests.java @@ -51,9 +51,10 @@ public void shouldReturnTransactionTypeForValidValues(String value) { @NullSource @ParameterizedTest @ArgumentsSource(value = TransactionTypeInvalidArgumentProvider.class) - public void shouldThrowIllegalArgumentExceptionForInvalidValues(String value) { - assertThrows(IllegalArgumentException.class, () -> TransactionType.forValue(value), - "No matching TransactionType enum value for String value " + value); + public void shouldReturnUnknownForInvalidValues(String value) { + TransactionType transactionType = TransactionType.forValue(value); + assertNotNull(transactionType); + assertThat(transactionType).isEqualTo(TransactionType.UNKNOWN); } public static class TransactionTypeValidArgumentProvider implements ArgumentsProvider { From d84447f0dcdcef413f1d3f90f03418281c4e3419 Mon Sep 17 00:00:00 2001 From: rsteimen Date: Tue, 6 Feb 2024 07:35:19 +0100 Subject: [PATCH 2/2] Removed unnecessary \n in json test data string --- .../binary/BinarySerializationTests.java | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java index 6824e47a6..a011c8cac 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java @@ -2125,30 +2125,30 @@ void serializeDidDelete() throws JsonProcessingException { @Test void deserializeUnknown() throws JsonProcessingException { - String json = "{\n" + - " \"Account\": \"rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8\",\n" + - " \"Fee\": \"10\",\n" + - " \"Sequence\": 4,\n" + - " \"TransactionType\": \"UnknownCustom\",\n" + - " \"UnknownProperty\": \"value123\",\n" + - " \"UnknownObject\": {\n" + - " \"name\": \"value0\",\n" + - " \"key1\": 1\n" + - " },\n" + - " \"UnknownArray\": [\n" + - " {\n" + - " \"UnknownDetail\": {\n" + - " \"Amount\": \"1666671963\",\n" + - " \"Destination\": \"rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"UnknownDetail\": {\n" + - " \"Amount\": \"83333166\",\n" + - " \"Destination\": \"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\"\n" + - " }\n" + - " }\n" + - " ]\n" + + String json = "{" + + " \"Account\": \"rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8\"," + + " \"Fee\": \"10\"," + + " \"Sequence\": 4," + + " \"TransactionType\": \"UnknownCustom\"," + + " \"UnknownProperty\": \"value123\"," + + " \"UnknownObject\": {" + + " \"name\": \"value0\"," + + " \"key1\": 1" + + " }," + + " \"UnknownArray\": [" + + " {" + + " \"UnknownDetail\": {" + + " \"Amount\": \"1666671963\"," + + " \"Destination\": \"rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL\"" + + " }" + + " }," + + " {" + + " \"UnknownDetail\": {" + + " \"Amount\": \"83333166\"," + + " \"Destination\": \"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\"" + + " }" + + " }" + + " ]" + "}"; UnknownTransaction transaction = objectMapper.readValue(json, UnknownTransaction.class);