Skip to content

Commit 3153d8d

Browse files
nkramer44sappenin
andauthored
Add binary codec support for MPT amounts and STHash192 (XRPLF#556)
* Add MPT Support (XRPLF#558) * Add all MPT transaction, ledger, and meta objects. * Add ITs for MPT --------- Co-authored-by: David Fuelling <[email protected]>
1 parent 10a6e2a commit 3153d8d

File tree

78 files changed

+5429
-1326
lines changed

Some content is hidden

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

78 files changed

+5429
-1326
lines changed

xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult;
6969
import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams;
7070
import org.xrpl.xrpl4j.model.client.ledger.LedgerResult;
71+
import org.xrpl.xrpl4j.model.client.mpt.MptHoldersRequestParams;
72+
import org.xrpl.xrpl4j.model.client.mpt.MptHoldersResponse;
7173
import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersRequestParams;
7274
import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersResult;
7375
import org.xrpl.xrpl4j.model.client.nft.NftInfoRequestParams;
@@ -873,6 +875,24 @@ public GetAggregatePriceResult getAggregatePrice(
873875
return jsonRpcClient.send(request, GetAggregatePriceResult.class);
874876
}
875877

878+
/**
879+
* Get all holders of an MPT and their balance. The mpt_holders method is only available on Clio nodes.
880+
*
881+
* @param params An {@link MptHoldersRequestParams}.
882+
*
883+
* @return An {@link MptHoldersResponse}.
884+
*
885+
* @throws JsonRpcClientErrorException if {@code js nRpcClient} throws an error.
886+
*/
887+
public MptHoldersResponse mptHolders(MptHoldersRequestParams params) throws JsonRpcClientErrorException {
888+
JsonRpcRequest request = JsonRpcRequest.builder()
889+
.method(XrplMethods.MPT_HOLDERS)
890+
.addParams(params)
891+
.build();
892+
893+
return jsonRpcClient.send(request, MptHoldersResponse.class);
894+
}
895+
876896
public JsonRpcClient getJsonRpcClient() {
877897
return jsonRpcClient;
878898
}

xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181
import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult;
8282
import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams;
8383
import org.xrpl.xrpl4j.model.client.ledger.LedgerResult;
84+
import org.xrpl.xrpl4j.model.client.mpt.MptHoldersRequestParams;
85+
import org.xrpl.xrpl4j.model.client.mpt.MptHoldersResponse;
8486
import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersRequestParams;
8587
import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersResult;
8688
import org.xrpl.xrpl4j.model.client.nft.NftInfoRequestParams;
@@ -1121,4 +1123,22 @@ void getAggregatePrice() throws JsonRpcClientErrorException {
11211123

11221124
assertThat(result).isEqualTo(expectedResult);
11231125
}
1126+
1127+
@Test
1128+
void mptHolders() throws JsonRpcClientErrorException {
1129+
MptHoldersRequestParams params = mock(MptHoldersRequestParams.class);
1130+
MptHoldersResponse expectedResult = mock(MptHoldersResponse.class);
1131+
1132+
when(jsonRpcClientMock.send(
1133+
JsonRpcRequest.builder()
1134+
.method(XrplMethods.MPT_HOLDERS)
1135+
.addParams(params)
1136+
.build(),
1137+
MptHoldersResponse.class
1138+
)).thenReturn(expectedResult);
1139+
1140+
MptHoldersResponse result = xrplClient.mptHolders(params);
1141+
1142+
assertThat(result).isEqualTo(expectedResult);
1143+
}
11241144
}

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/serdes/BinarySerializer.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,8 @@ public void writeFieldAndValue(final FieldInstance field, final SerializedType v
107107
public void writeFieldAndValue(final FieldInstance field, final JsonNode value) throws JsonProcessingException {
108108
Objects.requireNonNull(field);
109109
Objects.requireNonNull(value);
110-
SerializedType typedValue;
111-
if (field.name().equals("BaseFee")) {
112-
typedValue = SerializedType.getTypeByName(field.type()).fromHex(value.asText());
113-
} else {
114-
typedValue = SerializedType.getTypeByName(field.type()).fromJson(value);
115-
}
110+
SerializedType<?> typedValue = SerializedType.getTypeByName(field.type()).fromJson(value, field);
111+
116112
writeFieldAndValue(field, typedValue);
117113
}
118114

@@ -121,7 +117,7 @@ public void writeFieldAndValue(final FieldInstance field, final JsonNode value)
121117
*
122118
* @param value length encoded value to write to BytesList.
123119
*/
124-
public void writeLengthEncoded(final SerializedType value) {
120+
public void writeLengthEncoded(final SerializedType<?> value) {
125121
Objects.requireNonNull(value);
126122
UnsignedByteArray bytes = UnsignedByteArray.empty();
127123
value.toBytesSink(bytes);

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/AmountType.java

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import org.xrpl.xrpl4j.codec.binary.math.MathUtils;
3333
import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser;
3434
import org.xrpl.xrpl4j.model.immutables.FluentCompareTo;
35+
import org.xrpl.xrpl4j.model.transactions.MpTokenIssuanceId;
36+
import org.xrpl.xrpl4j.model.transactions.MptCurrencyAmount;
3537

3638
import java.math.BigDecimal;
3739
import java.math.BigInteger;
@@ -50,6 +52,7 @@ class AmountType extends SerializedType<AmountType> {
5052
public static final String ZERO_CURRENCY_AMOUNT_HEX = "8000000000000000";
5153
public static final int NATIVE_AMOUNT_BYTE_LENGTH = 8;
5254
public static final int CURRENCY_AMOUNT_BYTE_LENGTH = 48;
55+
public static final int MPT_AMOUNT_BYTE_LENGTH = 33;
5356
private static final int MAX_IOU_PRECISION = 16;
5457

5558
/**
@@ -142,14 +145,28 @@ private static void verifyNoDecimal(BigDecimal decimal) {
142145

143146
@Override
144147
public AmountType fromParser(BinaryParser parser) {
145-
boolean isXrp = !parser.peek().isNthBitSet(1);
146-
int numBytes = isXrp ? NATIVE_AMOUNT_BYTE_LENGTH : CURRENCY_AMOUNT_BYTE_LENGTH;
148+
UnsignedByte nextByte = parser.peek();
149+
// The first bit is 0 for XRP or MPT, and 1 for IOU.
150+
boolean isIssuedCurrency = nextByte.isNthBitSet(1);
151+
152+
int numBytes;
153+
if (isIssuedCurrency) {
154+
numBytes = CURRENCY_AMOUNT_BYTE_LENGTH;
155+
} else {
156+
// The third bit is 1 for MPT, and 0 for XRP
157+
boolean isMpt = nextByte.isNthBitSet(3);
158+
159+
numBytes = isMpt ? MPT_AMOUNT_BYTE_LENGTH : NATIVE_AMOUNT_BYTE_LENGTH;
160+
}
161+
162+
// parse all bytes, including the token-type bytes peeked above.
147163
return new AmountType(parser.read(numBytes));
148164
}
149165

150166
@Override
151167
public AmountType fromJson(JsonNode value) throws JsonProcessingException {
152168
if (value.isValueNode()) {
169+
// XRP Amount
153170
assertXrpIsValid(value.asText());
154171

155172
final boolean isValueNegative = value.asText().startsWith("-");
@@ -166,22 +183,48 @@ public AmountType fromJson(JsonNode value) throws JsonProcessingException {
166183
rawBytes[0] |= 0x40;
167184
}
168185
return new AmountType(UnsignedByteArray.of(rawBytes));
169-
}
186+
} else if (!value.has("mpt_issuance_id")) {
187+
// IOU Amount
188+
Amount amount = objectMapper.treeToValue(value, Amount.class);
189+
BigDecimal number = new BigDecimal(amount.value());
170190

171-
Amount amount = objectMapper.treeToValue(value, Amount.class);
172-
BigDecimal number = new BigDecimal(amount.value());
191+
UnsignedByteArray result = number.unscaledValue().equals(BigInteger.ZERO) ?
192+
UnsignedByteArray.fromHex(ZERO_CURRENCY_AMOUNT_HEX) :
193+
getAmountBytes(number);
173194

174-
UnsignedByteArray result = number.unscaledValue().equals(BigInteger.ZERO) ?
175-
UnsignedByteArray.fromHex(ZERO_CURRENCY_AMOUNT_HEX) :
176-
getAmountBytes(number);
195+
UnsignedByteArray currency = new CurrencyType().fromJson(value.get("currency")).value();
196+
UnsignedByteArray issuer = new AccountIdType().fromJson(value.get("issuer")).value();
177197

178-
UnsignedByteArray currency = new CurrencyType().fromJson(value.get("currency")).value();
179-
UnsignedByteArray issuer = new AccountIdType().fromJson(value.get("issuer")).value();
198+
result.append(currency);
199+
result.append(issuer);
180200

181-
result.append(currency);
182-
result.append(issuer);
201+
return new AmountType(result);
202+
} else {
203+
// MPT Amount
204+
MptCurrencyAmount mptCurrencyAmount = objectMapper.treeToValue(value, MptCurrencyAmount.class);
205+
206+
if (FluentCompareTo.is(mptCurrencyAmount.unsignedLongValue()).greaterThan(UnsignedLong.valueOf(Long.MAX_VALUE))) {
207+
throw new IllegalArgumentException("Invalid MPT mptCurrencyAmount. Maximum MPT value is (2^63 - 1)");
208+
}
183209

184-
return new AmountType(result);
210+
UnsignedByteArray amountBytes = UnsignedByteArray.fromHex(
211+
ByteUtils.padded(
212+
mptCurrencyAmount.unsignedLongValue().toString(16),
213+
16 // <-- 64 / 4
214+
)
215+
);
216+
UnsignedByteArray issuanceIdBytes = new UInt192Type()
217+
.fromJson(new TextNode(mptCurrencyAmount.mptIssuanceId().value()))
218+
.value();
219+
220+
// MPT Amounts always have 0110_000 (0x60) as the first byte when positive or 0010_0000 (0x20) when negative.
221+
int leadingByte = mptCurrencyAmount.isNegative() ? 0x20 : 0x60;
222+
UnsignedByteArray result = UnsignedByteArray.of(UnsignedByte.of(leadingByte));
223+
result.append(amountBytes);
224+
result.append(issuanceIdBytes);
225+
226+
return new AmountType(result);
227+
}
185228
}
186229

187230
private UnsignedByteArray getAmountBytes(BigDecimal number) {
@@ -213,7 +256,23 @@ public JsonNode toJson() {
213256
value = value.negate();
214257
}
215258
return new TextNode(value.toString());
259+
} else if (this.isMpt()) {
260+
BinaryParser parser = new BinaryParser(this.toHex());
261+
// We know the first byte already based on this.isMpt()
262+
UnsignedByte leadingByte = parser.read(1).get(0);
263+
boolean isNegative = !leadingByte.isNthBitSet(2);
264+
UnsignedLong amount = parser.readUInt64();
265+
UnsignedByteArray issuanceId = new UInt192Type().fromParser(parser).value();
266+
267+
String amountBase10 = amount.toString(10);
268+
MptCurrencyAmount mptAmount = MptCurrencyAmount.builder()
269+
.value(isNegative ? "-" + amountBase10 : amountBase10)
270+
.mptIssuanceId(MpTokenIssuanceId.of(issuanceId.hexValue()))
271+
.build();
272+
273+
return objectMapper.valueToTree(mptAmount);
216274
} else {
275+
// Must be IOU if it's not XRP or MPT
217276
BinaryParser parser = new BinaryParser(this.toHex());
218277
UnsignedByteArray mantissa = parser.read(8);
219278
final SerializedType<?> currency = new CurrencyType().fromParser(parser);
@@ -250,17 +309,24 @@ public JsonNode toJson() {
250309
*
251310
* @return {@code true} if this AmountType is native; {@code false} otherwise.
252311
*/
253-
private boolean isNative() {
254-
// 1st bit in 1st byte is set to 0 for native XRP
255-
return (toBytes()[0] & 0x80) == 0;
312+
public boolean isNative() {
313+
// 1st bit in 1st byte is set to 0 for native XRP, 3rd bit is also 0.
314+
byte leadingByte = toBytes()[0];
315+
return (leadingByte & 0x80) == 0 && (leadingByte & 0x20) == 0;
316+
}
317+
318+
public boolean isMpt() {
319+
// 1st bit in 1st byte is 0, and 3rd bit is 1
320+
byte leadingByte = toBytes()[0];
321+
return (leadingByte & 0x80) == 0 && (leadingByte & 0x20) != 0;
256322
}
257323

258324
/**
259325
* Determines if this AmountType is positive.
260326
*
261327
* @return {@code true} if this AmountType is positive; {@code false} otherwise.
262328
*/
263-
private boolean isPositive() {
329+
public boolean isPositive() {
264330
// 2nd bit in 1st byte is set to 1 for positive amounts
265331
return (toBytes()[0] & 0x40) > 0;
266332
}

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/STObjectType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ public JsonNode toJson() {
181181
if (field.name().equals(OBJECT_END_MARKER)) {
182182
break;
183183
}
184-
JsonNode value = parser.readFieldValue(field).toJson();
184+
JsonNode value = parser.readFieldValue(field).toJson(field);
185185
JsonNode mapped = definitionsService.mapFieldRawValueToSpecialization(field.name(), value.asText())
186186
.map(TextNode::new)
187187
.map(JsonNode.class::cast)

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/SerializedType.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.common.collect.ImmutableMap;
2727
import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray;
2828
import org.xrpl.xrpl4j.codec.binary.BinaryCodecObjectMapperFactory;
29+
import org.xrpl.xrpl4j.codec.binary.definitions.FieldInstance;
2930
import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser;
3031

3132
import java.util.Map;
@@ -48,6 +49,7 @@ public abstract class SerializedType<T extends SerializedType<T>> {
4849
.put("Currency", () -> new CurrencyType())
4950
.put("Hash128", () -> new Hash128Type())
5051
.put("Hash160", () -> new Hash160Type())
52+
.put("Hash192", () -> new UInt192Type())
5153
.put("Hash256", () -> new Hash256Type())
5254
.put("PathSet", () -> new PathSetType())
5355
.put("STArray", () -> new STArrayType())
@@ -74,7 +76,11 @@ public SerializedType(UnsignedByteArray bytes) {
7476
* @return A {@link SerializedType} for the supplied {@code name}.
7577
*/
7678
public static SerializedType<?> getTypeByName(String name) {
77-
return typeMap.get(name).get();
79+
try {
80+
return typeMap.get(name).get();
81+
} catch (NullPointerException e) {
82+
throw e;
83+
}
7884
}
7985

8086
/**
@@ -117,15 +123,34 @@ public T fromParser(BinaryParser parser, int lengthHint) {
117123
}
118124

119125
/**
120-
* Obtain a {@link T} using the supplied {@code node}.
126+
* Obtain a {@link T} using the supplied {@code node}. Prefer using {@link #fromJson(JsonNode, FieldInstance)} over
127+
* this method, as some {@link SerializedType}s require a {@link FieldInstance} to accurately serialize and
128+
* deserialize.
121129
*
122130
* @param node A {@link JsonNode} to use.
123131
*
124132
* @return A {@link T} based upon the information found in {@code node}.
133+
*
125134
* @throws JsonProcessingException if {@code node} is not well-formed JSON.
126135
*/
127136
public abstract T fromJson(JsonNode node) throws JsonProcessingException;
128137

138+
/**
139+
* Obtain a {@link T} using the supplied {@link JsonNode} as well as a {@link FieldInstance}. Prefer using this method
140+
* where possible over {@link #fromJson(JsonNode)}, as some {@link SerializedType}s require a {@link FieldInstance} to
141+
* accurately serialize and deserialize.
142+
*
143+
* @param node A {@link JsonNode} to serialize to binary.
144+
* @param fieldInstance The {@link FieldInstance} describing the field being serialized.
145+
*
146+
* @return A {@link T}.
147+
*
148+
* @throws JsonProcessingException If {@code node} is not well-formed JSON.
149+
*/
150+
public T fromJson(JsonNode node, FieldInstance fieldInstance) throws JsonProcessingException {
151+
return fromJson(node);
152+
}
153+
129154
/**
130155
* Construct a concrete instance of {@link SerializedType} from the supplied {@code json}.
131156
*
@@ -189,14 +214,29 @@ public byte[] toBytes() {
189214
}
190215

191216
/**
192-
* Convert this {@link SerializedType} to a {@link JsonNode}.
217+
* Convert this {@link SerializedType} to a {@link JsonNode}. Prefer using {@link #toJson(FieldInstance)} over this
218+
* method where possible, as some {@link SerializedType}s require a {@link FieldInstance} to accurately serialize and
219+
* deserialize.
193220
*
194221
* @return A {@link JsonNode}.
195222
*/
196223
public JsonNode toJson() {
197224
return new TextNode(toHex());
198225
}
199226

227+
/**
228+
* Convert this {@link SerializedType} to a {@link JsonNode} based on the supplied {@link FieldInstance}. Prefer using
229+
* this method where possible over {@link #fromJson(JsonNode)}, as some {@link SerializedType}s require a
230+
* {@link FieldInstance} to accurately serialize and deserialize.
231+
*
232+
* @param fieldInstance A {@link FieldInstance} describing the field being deserialized.
233+
*
234+
* @return A {@link JsonNode}.
235+
*/
236+
public JsonNode toJson(FieldInstance fieldInstance) {
237+
return toJson();
238+
}
239+
200240
/**
201241
* Convert this {@link SerializedType} to a hex-encoded {@link String}.
202242
*
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.xrpl.xrpl4j.codec.binary.types;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray;
5+
import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser;
6+
7+
/**
8+
* Codec for XRPL UInt192 type.
9+
*/
10+
public class UInt192Type extends UIntType<UInt192Type> {
11+
12+
public static final int WIDTH_BYTES = 24;
13+
14+
public UInt192Type() {
15+
this(UnsignedByteArray.ofSize(WIDTH_BYTES));
16+
}
17+
18+
public UInt192Type(UnsignedByteArray list) {
19+
super(list, WIDTH_BYTES * 8);
20+
}
21+
22+
@Override
23+
public UInt192Type fromParser(BinaryParser parser) {
24+
return new UInt192Type(parser.read(WIDTH_BYTES));
25+
}
26+
27+
@Override
28+
public UInt192Type fromJson(JsonNode node) {
29+
return new UInt192Type(UnsignedByteArray.fromHex(node.asText()));
30+
}
31+
}

0 commit comments

Comments
 (0)