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);