Skip to content

Commit 4a01fd4

Browse files
authored
Fixes #590 (#594)
Fixes #590 by removing the `Account` property from any incoming `UnlModify` JSON about to be deserialized. This fixes #590 because the JSON returned by the rippled/clio API v1 has a bug where the account value in `UnlModify` transactions is an empty string, when serialized throws an exception because empty string is not a valid Address. By removing the property from incoming JSON, the Java value for the `Account` property is always set to ACCOUNT_ZERO via a default method. Without this fix, the `Account` will also errantly end up in the `unknownFields map of the ultimate Java object, which is incorrect.
1 parent a69b5f1 commit 4a01fd4

File tree

4 files changed

+10251
-34
lines changed

4 files changed

+10251
-34
lines changed

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

+19-5
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -27,6 +27,7 @@
2727
import com.fasterxml.jackson.databind.node.ObjectNode;
2828
import org.xrpl.xrpl4j.model.transactions.Transaction;
2929
import org.xrpl.xrpl4j.model.transactions.TransactionType;
30+
import org.xrpl.xrpl4j.model.transactions.UnlModify;
3031

3132
import java.io.IOException;
3233

@@ -45,10 +46,23 @@ protected TransactionDeserializer() {
4546

4647
@Override
4748
public Transaction deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
48-
ObjectMapper objectMapper = (ObjectMapper) jsonParser.getCodec();
49-
ObjectNode objectNode = objectMapper.readTree(jsonParser);
49+
final ObjectMapper objectMapper = (ObjectMapper) jsonParser.getCodec();
50+
final ObjectNode objectNode = objectMapper.readTree(jsonParser);
5051

5152
TransactionType transactionType = TransactionType.forValue(objectNode.get("TransactionType").asText());
52-
return objectMapper.treeToValue(objectNode, Transaction.typeMap.inverse().get(transactionType));
53+
final Class<? extends Transaction> transactionTypeClass = Transaction.typeMap.inverse().get(transactionType);
54+
55+
// Fixes #590 by removing the `Account` property from any incoming `UnlModify` JSON about to be deserialized.
56+
// This fixes #590 because the JSON returned by the rippled/clio API v1 has a bug where the account value in
57+
// `UnlModify` transactions is an empty string. When this value is deserialized, an exception is thrown because
58+
// the empty string value is not a valid `Address`. By removing the property from incoming JSON, the Java value
59+
// for the `Account` property is always set to ACCOUNT_ZERO via a default method. One other side effect of this
60+
// fix is that `Account` property will not be errantly added to `unknownFields map of the ultimate Java object,
61+
// which is incorrect.
62+
if (UnlModify.class.isAssignableFrom(transactionTypeClass)) {
63+
objectNode.remove("Account");
64+
}
65+
66+
return objectMapper.treeToValue(objectNode, transactionTypeClass);
5367
}
5468
}

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

+15-16
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -28,8 +28,8 @@
2828
import org.xrpl.xrpl4j.model.client.common.LedgerIndex;
2929

3030
/**
31-
* A {@link UnlModify} pseudo-transaction marks a change to the Negative UNL,
32-
* indicating that a trusted validator has gone offline or come back online.
31+
* A {@link UnlModify} pseudo-transaction marks a change to the Negative UNL, indicating that a trusted validator has
32+
* gone offline or come back online.
3333
*
3434
* @see "https://xrpl.org/unlmodify.html"
3535
*/
@@ -49,36 +49,35 @@ static ImmutableUnlModify.Builder builder() {
4949
return ImmutableUnlModify.builder();
5050
}
5151

52-
5352
/**
54-
* This field is overridden in this class because of a bug in rippled that causes this field to be missing
55-
* in API responses. In other pseudo-transactions such as {@link SetFee} and {@link EnableAmendment}, the rippled
56-
* API sets the {@code account} field to a special XRPL address called ACCOUNT_ZERO, which is the base58
57-
* encoding of the number zero. Because rippled does not set the {@code account} field of the {@link UnlModify}
58-
* pseudo-transaction, this override will always set the field to ACCOUNT_ZERO to avoid deserialization issues
59-
* and to be consistent with other pseudo-transactions.
53+
* This field is overridden in this class because of a bug in rippled that causes this field to be missing in API
54+
* responses. In other pseudo-transactions such as {@link SetFee} and {@link EnableAmendment}, the rippled API sets
55+
* the {@code account} field to a special XRPL address called ACCOUNT_ZERO, which is the base58 encoding of the number
56+
* zero. Because rippled does not set the {@code account} field of the {@link UnlModify} pseudo-transaction, this
57+
* override will always set the field to ACCOUNT_ZERO to avoid deserialization issues and to be consistent with other
58+
* pseudo-transactions.
6059
*
6160
* @return Always returns ACCOUNT_ZERO, which is the base58 encoding of the number zero.
6261
*/
6362
@Override
6463
@JsonProperty("Account")
65-
@Value.Default
64+
@Value.Default // Must be `Default` not `Derived`, else this field will be serialized into `unknownFields`.
6665
default Address account() {
6766
return ACCOUNT_ZERO;
6867
}
6968

7069
/**
71-
* The {@link LedgerIndex} where this pseudo-transaction appears.
72-
* This distinguishes the pseudo-transaction from other occurrences of the same change.
70+
* The {@link LedgerIndex} where this pseudo-transaction appears. This distinguishes the pseudo-transaction from other
71+
* occurrences of the same change.
7372
*
7473
* @return A {@link LedgerIndex} to indicates where the tx appears.
7574
*/
7675
@JsonProperty("LedgerSequence")
7776
LedgerIndex ledgerSequence();
7877

7978
/**
80-
* If 1, this change represents adding a validator to the Negative UNL. If 0, this change represents
81-
* removing a validator from the Negative UNL.
79+
* If 1, this change represents adding a validator to the Negative UNL. If 0, this change represents removing a
80+
* validator from the Negative UNL.
8281
*
8382
* @return An {@link UnsignedInteger} denoting either 0 or 1.
8483
*/

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

+57-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.xrpl.xrpl4j.model.transactions;
22

33
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
4-
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
54

65
import com.fasterxml.jackson.databind.ObjectMapper;
76
import org.junit.jupiter.api.Test;
@@ -112,6 +111,29 @@ void deserializeLedgerResultWithNegativeAmounts(String ledgerResultFileName) thr
112111
});
113112
}
114113

114+
/**
115+
* This test validates that the ledger 94084608 and all of its transactions and metadata are handled correctly, even
116+
* in the presence of a `UnlModify` transaction that has an empty `Account`.
117+
*/
118+
@ParameterizedTest
119+
@ValueSource(strings = {
120+
"ledger-result-94084608.json" // <-- See https://github.com/XRPLF/xrpl4j/issues/590
121+
})
122+
void deserializeLedgerResultWithSpecialObjects(String ledgerResultFileName) throws IOException {
123+
Objects.requireNonNull(ledgerResultFileName);
124+
125+
File jsonFile = new File(
126+
"src/test/resources/special-object-ledgers/" + ledgerResultFileName
127+
);
128+
129+
LedgerResult ledgerResult = objectMapper.readValue(jsonFile, LedgerResult.class);
130+
131+
ledgerResult.ledger().transactions().forEach(transactionResult -> {
132+
assertThat(transactionResult.metadata().isPresent()).isTrue();
133+
transactionResult.metadata().ifPresent(this::handleTransactionMetadata);
134+
});
135+
}
136+
115137
/**
116138
* This test validates that the ledger 87704323 and all of its transactions and metadata are handled correctly, even
117139
* in the presence of negative XRP or IOU amounts.
@@ -144,9 +166,13 @@ private void handleTransactionMetadata(final TransactionMetadata transactionMeta
144166
} else if (ledgerEntryType.equals(MetaLedgerEntryType.RIPPLE_STATE)) {
145167
handleMetaLedgerObject((MetaRippleStateObject) createdNode.newFields());
146168
} else if (ledgerEntryType.equals(MetaLedgerEntryType.DIRECTORY_NODE)) {
147-
logger.warn("Ignoring ledger entry type {}", ledgerEntryType);
169+
logger.warn("Ignoring CreatedNode ledger entry type {}", ledgerEntryType);
170+
} else if (ledgerEntryType.equals(MetaLedgerEntryType.NEGATIVE_UNL)) {
171+
logger.warn(
172+
"Ignoring DeletedNode ledger entry type {}. See https://github.com/XRPLF/xrpl4j/issues/16",
173+
ledgerEntryType);
148174
} else {
149-
throw new RuntimeException("Unhandled ledger entry type: " + ledgerEntryType);
175+
throw new RuntimeException("Unhandled CreatedNode ledger entry type: " + ledgerEntryType);
150176
}
151177
},
152178
(modifiedNode) -> {
@@ -159,8 +185,19 @@ private void handleTransactionMetadata(final TransactionMetadata transactionMeta
159185
handleMetaLedgerObject((MetaAccountRootObject) metaLedgerObject);
160186
} else if (ledgerEntryType.equals(MetaLedgerEntryType.RIPPLE_STATE)) {
161187
handleMetaLedgerObject((MetaRippleStateObject) metaLedgerObject);
188+
} else if (ledgerEntryType.equals(MetaLedgerEntryType.DIRECTORY_NODE)) {
189+
logger.warn("Ignoring ModifiedNode ledger entry type {}", ledgerEntryType);
190+
} else if (ledgerEntryType.equals(MetaLedgerEntryType.NEGATIVE_UNL)) {
191+
logger.warn(
192+
"Ignoring DeletedNode ledger entry type {}. See https://github.com/XRPLF/xrpl4j/issues/16",
193+
ledgerEntryType);
194+
} else if (ledgerEntryType.equals(MetaLedgerEntryType.AMM)) {
195+
logger.warn(
196+
"Ignoring DeletedNode ledger entry type {}. See https://github.com/XRPLF/xrpl4j/issues/591",
197+
ledgerEntryType);
162198
} else {
163-
throw new RuntimeException("Unhandled ledger entry type: " + ledgerEntryType);
199+
throw new RuntimeException(
200+
"Unhandled ModifiedNode PreviousFields ledger entry type: " + ledgerEntryType);
164201
}
165202
});
166203

@@ -173,10 +210,15 @@ private void handleTransactionMetadata(final TransactionMetadata transactionMeta
173210
handleMetaLedgerObject((MetaAccountRootObject) metaLedgerObject);
174211
} else if (ledgerEntryType.equals(MetaLedgerEntryType.RIPPLE_STATE)) {
175212
handleMetaLedgerObject((MetaRippleStateObject) metaLedgerObject);
176-
} else if (ledgerEntryType.equals(MetaLedgerEntryType.DIRECTORY_NODE)) {
177-
logger.warn("Ignoring ledger entry type {}", ledgerEntryType);
213+
} else if (
214+
ledgerEntryType.equals(MetaLedgerEntryType.DIRECTORY_NODE)) {
215+
logger.warn("Ignoring ModifiedNode ledger entry type {}", ledgerEntryType);
216+
} else if (ledgerEntryType.equals(MetaLedgerEntryType.AMM)) {
217+
logger.warn(
218+
"Ignoring ModifiedNode ledger entry type {}. See See https://github.com/XRPLF/xrpl4j/issues/591",
219+
ledgerEntryType);
178220
} else {
179-
throw new RuntimeException("Unhandled ledger entry type: " + ledgerEntryType);
221+
throw new RuntimeException("Unhandled ModifiedNode FinalFields ledger entry type: " + ledgerEntryType);
180222
}
181223
});
182224
},
@@ -191,7 +233,8 @@ private void handleTransactionMetadata(final TransactionMetadata transactionMeta
191233
} else if (ledgerEntryType.equals(MetaLedgerEntryType.RIPPLE_STATE)) {
192234
handleMetaLedgerObject((MetaRippleStateObject) metaLedgerObject);
193235
} else {
194-
throw new RuntimeException("Unhandled ledger entry type: " + ledgerEntryType);
236+
throw new RuntimeException(
237+
"Unhandled DeletedNode PreviousFields ledger entry type: " + ledgerEntryType);
195238
}
196239
});
197240

@@ -204,14 +247,15 @@ private void handleTransactionMetadata(final TransactionMetadata transactionMeta
204247
} else if (ledgerEntryType.equals(MetaLedgerEntryType.RIPPLE_STATE)) {
205248
handleMetaLedgerObject((MetaRippleStateObject) finalFields);
206249
} else if (ledgerEntryType.equals(MetaLedgerEntryType.TICKET)) {
207-
logger.info("Ignoring ledger entry type {} because it has no currency values for negative checking",
208-
ledgerEntryType);
250+
logger.info(
251+
"Ignoring ledger entry type {} because it has no currency values for negative checking", ledgerEntryType
252+
);
209253
} else if (ledgerEntryType.equals(MetaLedgerEntryType.DIRECTORY_NODE)) {
210-
logger.warn("Ignoring ledger entry type {}", ledgerEntryType);
254+
logger.warn("Ignoring DeletedNode ledger entry type {}", ledgerEntryType);
211255
} else if (ledgerEntryType.equals(MetaLedgerEntryType.NFTOKEN_OFFER)) {
212256
handleMetaLedgerObject((MetaNfTokenOfferObject) finalFields);
213257
} else {
214-
throw new RuntimeException("Unhandled ledger entry type: " + ledgerEntryType);
258+
throw new RuntimeException("Unhandled DeletedNode FinalFields ledger entry type: " + ledgerEntryType);
215259
}
216260
}
217261
);
@@ -292,4 +336,4 @@ private void handleMetaLedgerObject(MetaNfTokenOfferObject metaNfTokenOfferObjec
292336
issuedCurrencyAmount.value().startsWith("-"))
293337
));
294338
}
295-
}
339+
}

0 commit comments

Comments
 (0)