Skip to content

Commit a69b5f1

Browse files
authored
Add support for unknown transactions (#569)
Add support for unknown transactions.
1 parent 613339d commit a69b5f1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1727
-42
lines changed

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/TransactionFlags.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ public class TransactionFlags extends Flags {
4949
TransactionFlags() {
5050
}
5151

52+
public static TransactionFlags of(long value) {
53+
return new TransactionFlags(value);
54+
}
55+
5256
/**
5357
* Flags indicating that a fully-canonical signature is required. This flag is highly recommended.
5458
*

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AccountTransactionsTransactionDeserializer.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import com.fasterxml.jackson.databind.JsonNode;
2626
import com.fasterxml.jackson.databind.ObjectMapper;
2727
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
28+
import com.fasterxml.jackson.databind.node.ObjectNode;
29+
import com.google.common.collect.Lists;
30+
import com.google.common.collect.Sets;
2831
import com.google.common.primitives.UnsignedInteger;
2932
import com.google.common.primitives.UnsignedLong;
3033
import org.xrpl.xrpl4j.model.client.accounts.AccountTransactionsTransaction;
@@ -33,14 +36,18 @@
3336
import org.xrpl.xrpl4j.model.transactions.Transaction;
3437

3538
import java.io.IOException;
39+
import java.util.ArrayList;
3640
import java.util.Optional;
41+
import java.util.Set;
3742

3843
/**
3944
* Custom Jackson Deserializer for {@link AccountTransactionsTransaction}s. This is necessary because Jackson
4045
* does not deserialize {@link com.fasterxml.jackson.annotation.JsonUnwrapped} fields intelligently.
4146
*/
4247
public class AccountTransactionsTransactionDeserializer extends StdDeserializer<AccountTransactionsTransaction<?>> {
4348

49+
public static final Set<String> EXTRA_TRANSACTION_FIELDS = Sets.newHashSet("ledger_index", "date", "hash");
50+
4451
/**
4552
* No-args constructor.
4653
*/
@@ -54,14 +61,20 @@ public AccountTransactionsTransaction<?> deserialize(
5461
DeserializationContext ctxt
5562
) throws IOException {
5663
ObjectMapper objectMapper = (ObjectMapper) jsonParser.getCodec();
57-
JsonNode node = objectMapper.readTree(jsonParser);
64+
ObjectNode node = objectMapper.readTree(jsonParser);
5865

59-
Transaction transaction = objectMapper.readValue(node.toString(), Transaction.class);
6066
long ledgerIndex = node.get("ledger_index").asLong(-1L);
6167
String hash = node.get("hash").asText();
6268
Optional<UnsignedLong> closeDate = Optional.ofNullable(node.get("date"))
6369
.map(JsonNode::asLong)
6470
.map(UnsignedLong::valueOf);
71+
72+
// The Transaction is @JsonUnwrapped in AccountTransactionsTransaction, which means these three fields
73+
// get added to the Transaction.unknownFields Map. To prevent that, we simply remove them from the JSON, because
74+
// they should only show up in AccountTransactionsTransaction
75+
node.remove(EXTRA_TRANSACTION_FIELDS);
76+
Transaction transaction = objectMapper.readValue(node.toString(), Transaction.class);
77+
6578
return AccountTransactionsTransaction.builder()
6679
.transaction(transaction)
6780
.ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(ledgerIndex)))

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/TransactionResultDeserializer.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.fasterxml.jackson.databind.ObjectMapper;
2828
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
2929
import com.fasterxml.jackson.databind.node.ObjectNode;
30+
import com.google.common.collect.Sets;
3031
import com.google.common.primitives.UnsignedInteger;
3132
import com.google.common.primitives.UnsignedLong;
3233
import org.xrpl.xrpl4j.model.client.common.LedgerIndex;
@@ -37,6 +38,7 @@
3738

3839
import java.io.IOException;
3940
import java.util.Optional;
41+
import java.util.Set;
4042

4143
/**
4244
* Custom deserializer for {@link TransactionResult}, which wraps the {@link Transaction} fields in the result JSON.
@@ -48,6 +50,10 @@
4850
*/
4951
public class TransactionResultDeserializer<T extends Transaction> extends StdDeserializer<TransactionResult<T>> {
5052

53+
public static final Set<String> EXTRA_TRANSACTION_FIELDS = Sets.newHashSet(
54+
"ledger_index", "date", "hash", "status", "validated", "meta", "metaData"
55+
);
56+
5157
/**
5258
* No-args constructor.
5359
*/
@@ -60,10 +66,6 @@ public TransactionResult<T> deserialize(JsonParser jsonParser, DeserializationCo
6066
ObjectMapper objectMapper = (ObjectMapper) jsonParser.getCodec();
6167
ObjectNode objectNode = objectMapper.readTree(jsonParser);
6268

63-
JavaType javaType = objectMapper.getTypeFactory().constructType(new TypeReference<T>() {
64-
});
65-
T transaction = objectMapper.convertValue(objectNode, javaType);
66-
6769
LedgerIndex ledgerIndex = objectNode.has("ledger_index") ?
6870
LedgerIndex.of(UnsignedInteger.valueOf(objectNode.get("ledger_index").asInt())) :
6971
null;
@@ -73,6 +75,15 @@ public TransactionResult<T> deserialize(JsonParser jsonParser, DeserializationCo
7375
Optional<TransactionMetadata> metadata = getTransactionMetadata(objectMapper, objectNode);
7476
UnsignedLong closeDate = objectNode.has("date") ? UnsignedLong.valueOf(objectNode.get("date").asLong()) : null;
7577

78+
// The Transaction is @JsonUnwrapped in TransactionResult, which means these fields
79+
// get added to the Transaction.unknownFields Map. To prevent that, we simply remove them from the JSON, because
80+
// they should only show up in AccountTransactionsTransaction
81+
objectNode.remove(EXTRA_TRANSACTION_FIELDS);
82+
83+
JavaType javaType = objectMapper.getTypeFactory().constructType(new TypeReference<T>() {
84+
});
85+
T transaction = objectMapper.convertValue(objectNode, javaType);
86+
7687
return TransactionResult.<T>builder()
7788
.transaction(transaction)
7889
.ledgerIndex(Optional.ofNullable(ledgerIndex))

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
* =========================LICENSE_END==================================
2121
*/
2222

23+
import com.fasterxml.jackson.annotation.JsonAnyGetter;
2324
import com.fasterxml.jackson.annotation.JsonInclude;
25+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
2426
import com.fasterxml.jackson.annotation.JsonProperty;
2527
import com.google.common.collect.BiMap;
2628
import com.google.common.collect.ImmutableBiMap;
@@ -30,6 +32,7 @@
3032
import org.xrpl.xrpl4j.crypto.signing.Signature;
3133

3234
import java.util.List;
35+
import java.util.Map;
3336
import java.util.Optional;
3437

3538
/**
@@ -90,6 +93,7 @@ public interface Transaction {
9093
.put(ImmutableDidDelete.class, TransactionType.DID_DELETE)
9194
.put(ImmutableOracleSet.class, TransactionType.ORACLE_SET)
9295
.put(ImmutableOracleDelete.class, TransactionType.ORACLE_DELETE)
96+
.put(ImmutableUnknownTransaction.class, TransactionType.UNKNOWN)
9397
.build();
9498

9599
/**
@@ -106,6 +110,7 @@ public interface Transaction {
106110
* @return A {@link TransactionType}.
107111
*/
108112
@JsonProperty("TransactionType")
113+
@Value.Default // must be Default rather than Derived, otherwise Jackson treats "TransactionType" as an unknownField
109114
default TransactionType transactionType() {
110115
return typeMap.get(this.getClass());
111116
}
@@ -220,4 +225,8 @@ default PublicKey signingPublicKey() {
220225
@JsonProperty("NetworkID")
221226
Optional<NetworkId> networkId();
222227

228+
@JsonAnyGetter
229+
@JsonInclude(Include.NON_ABSENT)
230+
Map<String, Object> unknownFields();
231+
223232
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,12 @@ public enum TransactionType {
336336
* is subject to change.</p>
337337
*/
338338
@Beta
339-
ORACLE_DELETE("OracleDelete");
339+
ORACLE_DELETE("OracleDelete"),
340+
341+
/**
342+
* The {@link TransactionType} for any transaction that is unrecognized/unsupported by xrpl4j.
343+
*/
344+
UNKNOWN("Unknown");
340345

341346
private final String value;
342347

@@ -358,7 +363,7 @@ public static TransactionType forValue(String value) {
358363
}
359364
}
360365

361-
throw new IllegalArgumentException("No matching TransactionType enum value for String value " + value);
366+
return UNKNOWN;
362367
}
363368

364369
/**
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.xrpl.xrpl4j.model.transactions;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnore;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
6+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
7+
import org.immutables.value.Value;
8+
import org.immutables.value.Value.Immutable;
9+
import org.xrpl.xrpl4j.model.flags.TransactionFlags;
10+
11+
/**
12+
* Mapping for any transaction type that is unrecognized/unsupported by xrpl4j.
13+
*/
14+
@Immutable
15+
@JsonSerialize(as = ImmutableUnknownTransaction.class)
16+
@JsonDeserialize(as = ImmutableUnknownTransaction.class)
17+
public interface UnknownTransaction extends Transaction {
18+
19+
/**
20+
* Construct a {@code UnknownTransaction} builder.
21+
*
22+
* @return An {@link ImmutableUnknownTransaction.Builder}.
23+
*/
24+
static ImmutableUnknownTransaction.Builder builder() {
25+
return ImmutableUnknownTransaction.builder();
26+
}
27+
28+
/**
29+
* The actual transaction type found in the {@code "TransactionType"} field of the transaction JSON.
30+
*
31+
* <p>This has to be a {@link String} because {@link Transaction#transactionType()} is a {@link TransactionType},
32+
* which only has an UNKNOWN variant. Because this method is also annotated with {@link JsonProperty} of
33+
* "TransactionType", this essentially overrides the "TransactionType" field in JSON, but {@link #transactionType()}
34+
* will always be {@link TransactionType#UNKNOWN} and this field will contain the actual "TransactionType" field.
35+
*
36+
* @return A {@link String} containing the transaction type from JSON.
37+
*/
38+
@JsonProperty("TransactionType")
39+
String unknownTransactionType();
40+
41+
/**
42+
* The {@link TransactionType} of this UnknownTransaction, which will always be {@link TransactionType#UNKNOWN}.
43+
* {@link #unknownTransactionType()} contains the actual transaction type value.
44+
*
45+
* @return {@link TransactionType#UNKNOWN}.
46+
*/
47+
@Override
48+
@JsonIgnore
49+
@Value.Derived
50+
default TransactionType transactionType() {
51+
return Transaction.super.transactionType();
52+
}
53+
54+
/**
55+
* A set of {@link TransactionFlags}.
56+
*
57+
* @return A {@link TransactionFlags}.
58+
*/
59+
@JsonProperty("Flags")
60+
@Value.Default
61+
default TransactionFlags flags() {
62+
return TransactionFlags.EMPTY;
63+
}
64+
65+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ static ImmutableUnlModify.Builder builder() {
6262
*/
6363
@Override
6464
@JsonProperty("Account")
65-
@Value.Derived
65+
@Value.Default
6666
default Address account() {
6767
return ACCOUNT_ZERO;
6868
}

xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmBidTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,4 +312,58 @@ void testJsonWithXrpAmountBidMinAndMax() throws JSONException, JsonProcessingExc
312312
"}";
313313
assertCanSerializeAndDeserialize(ammBid, json);
314314
}
315+
316+
@Test
317+
void testJsonWithUnknownFields() throws JSONException, JsonProcessingException {
318+
AmmBid bid = AmmBid.builder()
319+
.account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm"))
320+
.signingPublicKey(
321+
PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC")
322+
)
323+
.asset(Issue.XRP)
324+
.asset2(
325+
Issue.builder()
326+
.issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"))
327+
.currency("TST")
328+
.build()
329+
)
330+
.addAuthAccounts(
331+
AuthAccountWrapper.of(AuthAccount.of(Address.of("rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg"))),
332+
AuthAccountWrapper.of(AuthAccount.of(Address.of("rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv")))
333+
)
334+
.fee(XrpCurrencyAmount.ofDrops(10))
335+
.sequence(UnsignedInteger.valueOf(9))
336+
.putUnknownFields("Foo", "Bar")
337+
.build();
338+
339+
String json = "{\n" +
340+
" \"Foo\" : \"Bar\",\n" +
341+
" \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" +
342+
" \"Asset\" : {\n" +
343+
" \"currency\" : \"XRP\"\n" +
344+
" },\n" +
345+
" \"Asset2\" : {\n" +
346+
" \"currency\" : \"TST\",\n" +
347+
" \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" +
348+
" },\n" +
349+
" \"AuthAccounts\" : [\n" +
350+
" {\n" +
351+
" \"AuthAccount\" : {\n" +
352+
" \"Account\" : \"rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg\"\n" +
353+
" }\n" +
354+
" },\n" +
355+
" {\n" +
356+
" \"AuthAccount\" : {\n" +
357+
" \"Account\" : \"rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv\"\n" +
358+
" }\n" +
359+
" }\n" +
360+
" ],\n" +
361+
" \"Fee\" : \"10\",\n" +
362+
" \"Sequence\" : 9,\n" +
363+
" \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" +
364+
" \"TransactionType\" : \"AMMBid\"\n" +
365+
"}";
366+
367+
assertCanSerializeAndDeserialize(bid, json);
368+
}
315369
}

xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmCreateTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,44 @@ void testJsonWithNonZeroFlags() throws JSONException, JsonProcessingException {
127127

128128
assertCanSerializeAndDeserialize(ammCreate, json);
129129
}
130+
131+
@Test
132+
void testJsonWithUnknownFields() throws JSONException, JsonProcessingException {
133+
AmmCreate ammCreate = AmmCreate.builder()
134+
.account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm"))
135+
.amount(
136+
IssuedCurrencyAmount.builder()
137+
.currency("TST")
138+
.issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"))
139+
.value("25")
140+
.build()
141+
)
142+
.amount2(XrpCurrencyAmount.ofDrops(250000000))
143+
.fee(XrpCurrencyAmount.ofDrops(10))
144+
.sequence(UnsignedInteger.valueOf(6))
145+
.tradingFee(TradingFee.of(UnsignedInteger.valueOf(500)))
146+
.signingPublicKey(
147+
PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC")
148+
)
149+
.putUnknownFields("Foo", "Bar")
150+
.build();
151+
152+
String json = "{\n" +
153+
" \"Foo\" : \"Bar\",\n" +
154+
" \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" +
155+
" \"Amount\" : {\n" +
156+
" \"currency\" : \"TST\",\n" +
157+
" \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\",\n" +
158+
" \"value\" : \"25\"\n" +
159+
" },\n" +
160+
" \"Amount2\" : \"250000000\",\n" +
161+
" \"Fee\" : \"10\",\n" +
162+
" \"Sequence\" : 6,\n" +
163+
" \"TradingFee\" : 500,\n" +
164+
" \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" +
165+
" \"TransactionType\" : \"AMMCreate\"\n" +
166+
"}";
167+
168+
assertCanSerializeAndDeserialize(ammCreate, json);
169+
}
130170
}

0 commit comments

Comments
 (0)