diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/serdes/BinarySerializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/serdes/BinarySerializer.java index 2ef048ba2..8a53d50b3 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/serdes/BinarySerializer.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/serdes/BinarySerializer.java @@ -107,12 +107,8 @@ public void writeFieldAndValue(final FieldInstance field, final SerializedType v public void writeFieldAndValue(final FieldInstance field, final JsonNode value) throws JsonProcessingException { Objects.requireNonNull(field); Objects.requireNonNull(value); - SerializedType typedValue; - if (field.name().equals("BaseFee")) { - typedValue = SerializedType.getTypeByName(field.type()).fromHex(value.asText()); - } else { - typedValue = SerializedType.getTypeByName(field.type()).fromJson(value); - } + SerializedType typedValue = SerializedType.getTypeByName(field.type()).fromJson(value, field); + writeFieldAndValue(field, typedValue); } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/AmountType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/AmountType.java index a186dc568..75d44533e 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/AmountType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/AmountType.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.base.Strings; import com.google.common.primitives.UnsignedLong; import org.xrpl.xrpl4j.codec.addresses.ByteUtils; import org.xrpl.xrpl4j.codec.addresses.UnsignedByte; @@ -50,6 +51,7 @@ class AmountType extends SerializedType { public static final String ZERO_CURRENCY_AMOUNT_HEX = "8000000000000000"; public static final int NATIVE_AMOUNT_BYTE_LENGTH = 8; public static final int CURRENCY_AMOUNT_BYTE_LENGTH = 48; + public static final int MPT_AMOUNT_BYTE_LENGTH = 33; private static final int MAX_IOU_PRECISION = 16; /** @@ -142,14 +144,23 @@ private static void verifyNoDecimal(BigDecimal decimal) { @Override public AmountType fromParser(BinaryParser parser) { - boolean isXrp = !parser.peek().isNthBitSet(1); - int numBytes = isXrp ? NATIVE_AMOUNT_BYTE_LENGTH : CURRENCY_AMOUNT_BYTE_LENGTH; + UnsignedByte nextByte = parser.peek(); + int numBytes; + if (nextByte.isNthBitSet(1)) { + numBytes = CURRENCY_AMOUNT_BYTE_LENGTH; + } else { + boolean isMpt = nextByte.isNthBitSet(3); + + numBytes = isMpt ? MPT_AMOUNT_BYTE_LENGTH : NATIVE_AMOUNT_BYTE_LENGTH; + } + return new AmountType(parser.read(numBytes)); } @Override public AmountType fromJson(JsonNode value) throws JsonProcessingException { if (value.isValueNode()) { + // XRP Amount assertXrpIsValid(value.asText()); final boolean isValueNegative = value.asText().startsWith("-"); @@ -166,22 +177,46 @@ public AmountType fromJson(JsonNode value) throws JsonProcessingException { rawBytes[0] |= 0x40; } return new AmountType(UnsignedByteArray.of(rawBytes)); - } + } else if (!value.has("mpt_issuance_id")) { + // IOU Amount + Amount amount = objectMapper.treeToValue(value, Amount.class); + BigDecimal number = new BigDecimal(amount.value()); - Amount amount = objectMapper.treeToValue(value, Amount.class); - BigDecimal number = new BigDecimal(amount.value()); + UnsignedByteArray result = number.unscaledValue().equals(BigInteger.ZERO) ? + UnsignedByteArray.fromHex(ZERO_CURRENCY_AMOUNT_HEX) : + getAmountBytes(number); - UnsignedByteArray result = number.unscaledValue().equals(BigInteger.ZERO) ? - UnsignedByteArray.fromHex(ZERO_CURRENCY_AMOUNT_HEX) : - getAmountBytes(number); + UnsignedByteArray currency = new CurrencyType().fromJson(value.get("currency")).value(); + UnsignedByteArray issuer = new AccountIdType().fromJson(value.get("issuer")).value(); + + result.append(currency); + result.append(issuer); + + return new AmountType(result); + } else { + // MPT Amount + MptAmount amount = objectMapper.treeToValue(value, MptAmount.class); + + if (FluentCompareTo.is(amount.unsignedLongValue()).greaterThan(UnsignedLong.valueOf(Long.MAX_VALUE))) { + throw new IllegalArgumentException("Invalid MPT amount. Maximum MPT value is (2^63 - 1)"); + } - UnsignedByteArray currency = new CurrencyType().fromJson(value.get("currency")).value(); - UnsignedByteArray issuer = new AccountIdType().fromJson(value.get("issuer")).value(); + UnsignedByteArray amountBytes = UnsignedByteArray.fromHex( + ByteUtils.padded( + amount.unsignedLongValue().toString(16), + 16 // <-- 64 / 4 + ) + ); + UnsignedByteArray issuanceIdBytes = new UInt192Type().fromJson(new TextNode(amount.mptIssuanceId())).value(); - result.append(currency); - result.append(issuer); + // MPT Amounts always have 0110000 as its first byte. + int leadingByte = amount.isNegative() ? 0x20 : 0x60; + UnsignedByteArray result = UnsignedByteArray.of(UnsignedByte.of(leadingByte)); + result.append(amountBytes); + result.append(issuanceIdBytes); - return new AmountType(result); + return new AmountType(result); + } } private UnsignedByteArray getAmountBytes(BigDecimal number) { @@ -213,7 +248,23 @@ public JsonNode toJson() { value = value.negate(); } return new TextNode(value.toString()); + } else if (this.isMpt()) { + BinaryParser parser = new BinaryParser(this.toHex()); + // We know the first byte already based on this.isMpt() + UnsignedByte leadingByte = parser.read(1).get(0); + boolean isNegative = !leadingByte.isNthBitSet(2); + UnsignedLong amount = parser.readUInt64(); + UnsignedByteArray issuanceId = new UInt192Type().fromParser(parser).value(); + + String amountBase10 = amount.toString(10); + MptAmount mptAmount = MptAmount.builder() + .value(isNegative ? "-" + amountBase10 : amountBase10) + .mptIssuanceId(issuanceId.hexValue()) + .build(); + + return objectMapper.valueToTree(mptAmount); } else { + // Must be IOU if it's not XRP or MPT BinaryParser parser = new BinaryParser(this.toHex()); UnsignedByteArray mantissa = parser.read(8); final SerializedType currency = new CurrencyType().fromParser(parser); @@ -250,9 +301,16 @@ public JsonNode toJson() { * * @return {@code true} if this AmountType is native; {@code false} otherwise. */ - private boolean isNative() { - // 1st bit in 1st byte is set to 0 for native XRP - return (toBytes()[0] & 0x80) == 0; + public boolean isNative() { + // 1st bit in 1st byte is set to 0 for native XRP, 3rd bit is also 0. + byte leadingByte = toBytes()[0]; + return (leadingByte & 0x80) == 0 && (leadingByte & 0x20) == 0; + } + + public boolean isMpt() { + // 1st bit in 1st byte is 0, and 3rd bit is 1 + byte leadingByte = toBytes()[0]; + return (leadingByte & 0x80) == 0 && (leadingByte & 0x20) != 0; } /** @@ -260,7 +318,7 @@ private boolean isNative() { * * @return {@code true} if this AmountType is positive; {@code false} otherwise. */ - private boolean isPositive() { + public boolean isPositive() { // 2nd bit in 1st byte is set to 1 for positive amounts return (toBytes()[0] & 0x40) > 0; } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/MptAmount.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/MptAmount.java new file mode 100644 index 000000000..fbbaffe6f --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/MptAmount.java @@ -0,0 +1,66 @@ +package org.xrpl.xrpl4j.codec.binary.types; + +/*- + * ========================LICENSE_START================================= + * xrpl4j :: binary-codec + * %% + * Copyright (C) 2020 - 2022 XRPL Foundation and its contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.primitives.UnsignedLong; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Model for XRPL MPT Amount JSON. + */ +@Immutable +@JsonSerialize(as = ImmutableMptAmount.class) +@JsonDeserialize(as = ImmutableMptAmount.class) +interface MptAmount { + + /** + * Construct a {@code MptAmount} builder. + * + * @return An {@link ImmutableMptAmount.Builder}. + */ + static ImmutableMptAmount.Builder builder() { + return ImmutableMptAmount.builder(); + } + + String value(); + + @JsonProperty("mpt_issuance_id") + String mptIssuanceId(); + + @Value.Derived + @JsonIgnore + default boolean isNegative() { + return value().startsWith("-"); + } + + @Value.Derived + @JsonIgnore + default UnsignedLong unsignedLongValue() { + return isNegative() ? + UnsignedLong.valueOf(value().substring(1)) : + UnsignedLong.valueOf(value()); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/STObjectType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/STObjectType.java index e1f3f4927..85d04a1d6 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/STObjectType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/STObjectType.java @@ -181,7 +181,7 @@ public JsonNode toJson() { if (field.name().equals(OBJECT_END_MARKER)) { break; } - JsonNode value = parser.readFieldValue(field).toJson(); + JsonNode value = parser.readFieldValue(field).toJson(field); JsonNode mapped = definitionsService.mapFieldRawValueToSpecialization(field.name(), value.asText()) .map(TextNode::new) .map(JsonNode.class::cast) diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/SerializedType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/SerializedType.java index 42ad767d9..0d86b9a76 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/SerializedType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/SerializedType.java @@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableMap; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.codec.binary.BinaryCodecObjectMapperFactory; +import org.xrpl.xrpl4j.codec.binary.definitions.FieldInstance; import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser; import java.util.Map; @@ -48,6 +49,7 @@ public abstract class SerializedType> { .put("Currency", () -> new CurrencyType()) .put("Hash128", () -> new Hash128Type()) .put("Hash160", () -> new Hash160Type()) + .put("UInt192", () -> new UInt192Type()) .put("Hash256", () -> new Hash256Type()) .put("PathSet", () -> new PathSetType()) .put("STArray", () -> new STArrayType()) @@ -117,15 +119,34 @@ public T fromParser(BinaryParser parser, int lengthHint) { } /** - * Obtain a {@link T} using the supplied {@code node}. + * Obtain a {@link T} using the supplied {@code node}. Prefer using {@link #fromJson(JsonNode, FieldInstance)} over + * this method, as some {@link SerializedType}s require a {@link FieldInstance} to accurately serialize and + * deserialize. * * @param node A {@link JsonNode} to use. * * @return A {@link T} based upon the information found in {@code node}. + * * @throws JsonProcessingException if {@code node} is not well-formed JSON. */ public abstract T fromJson(JsonNode node) throws JsonProcessingException; + /** + * Obtain a {@link T} using the supplied {@link JsonNode} as well as a {@link FieldInstance}. Prefer using this method + * where possible over {@link #fromJson(JsonNode)}, as some {@link SerializedType}s require a {@link FieldInstance} to + * accurately serialize and deserialize. + * + * @param node A {@link JsonNode} to serialize to binary. + * @param fieldInstance The {@link FieldInstance} describing the field being serialized. + * + * @return A {@link T}. + * + * @throws JsonProcessingException If {@code node} is not well-formed JSON. + */ + public T fromJson(JsonNode node, FieldInstance fieldInstance) throws JsonProcessingException { + return fromJson(node); + } + /** * Construct a concrete instance of {@link SerializedType} from the supplied {@code json}. * @@ -189,7 +210,9 @@ public byte[] toBytes() { } /** - * Convert this {@link SerializedType} to a {@link JsonNode}. + * Convert this {@link SerializedType} to a {@link JsonNode}. Prefer using {@link #toJson(FieldInstance)} over this + * method where possible, as some {@link SerializedType}s require a {@link FieldInstance} to accurately serialize and + * deserialize. * * @return A {@link JsonNode}. */ @@ -197,6 +220,19 @@ public JsonNode toJson() { return new TextNode(toHex()); } + /** + * Convert this {@link SerializedType} to a {@link JsonNode} based on the supplied {@link FieldInstance}. Prefer using + * this method where possible over {@link #fromJson(JsonNode)}, as some {@link SerializedType}s require a + * {@link FieldInstance} to accurately serialize and deserialize. + * + * @param fieldInstance A {@link FieldInstance} describing the field being deserialized. + * + * @return A {@link JsonNode}. + */ + public JsonNode toJson(FieldInstance fieldInstance) { + return toJson(); + } + /** * Convert this {@link SerializedType} to a hex-encoded {@link String}. * diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UInt192Type.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UInt192Type.java new file mode 100644 index 000000000..5fcdd276d --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UInt192Type.java @@ -0,0 +1,31 @@ +package org.xrpl.xrpl4j.codec.binary.types; + +import com.fasterxml.jackson.databind.JsonNode; +import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; +import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser; + +/** + * Codec for XRPL UInt192 type. + */ +public class UInt192Type extends UIntType { + + public static final int WIDTH_BYTES = 24; + + public UInt192Type() { + this(UnsignedByteArray.ofSize(WIDTH_BYTES)); + } + + public UInt192Type(UnsignedByteArray list) { + super(list, WIDTH_BYTES * 8); + } + + @Override + public UInt192Type fromParser(BinaryParser parser) { + return new UInt192Type(parser.read(WIDTH_BYTES)); + } + + @Override + public UInt192Type fromJson(JsonNode node) { + return new UInt192Type(UnsignedByteArray.fromHex(node.asText())); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UInt64Type.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UInt64Type.java index a08dfc05f..d58843e93 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UInt64Type.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UInt64Type.java @@ -9,9 +9,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,14 +22,25 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.collect.Sets; import com.google.common.primitives.UnsignedLong; +import org.xrpl.xrpl4j.codec.binary.definitions.FieldInstance; import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser; +import java.util.Set; + /** * Codec for XRPL UInt64 type. */ public class UInt64Type extends UIntType { + /** + * These fields are represented as base 10 Strings in JSON, whereas all other STUInt64s are represented in base16. + */ + protected static final Set BASE_10_UINT64_FIELD_NAMES = Sets.newHashSet( + "MaximumAmount", "OutstandingAmount", "MPTAmount" + ); + public UInt64Type() { this(UnsignedLong.ZERO); } @@ -45,12 +56,41 @@ public UInt64Type fromParser(BinaryParser parser) { @Override public UInt64Type fromJson(JsonNode value) { - // STUInt64s are represented as hex-encoded Strings in JSON. - return new UInt64Type(UnsignedLong.valueOf(value.asText(), 16)); + throw new UnsupportedOperationException("Cannot construct UInt64Type from JSON without a FieldInstance. Call " + + "the overload of this method that accepts a FieldInstance instead."); + } + + @Override + public UInt64Type fromJson(JsonNode value, FieldInstance fieldInstance) { + int radix = getRadix(fieldInstance); + return new UInt64Type(UnsignedLong.valueOf(value.asText(), radix)); } @Override public JsonNode toJson() { - return new TextNode(UnsignedLong.valueOf(toHex(), 16).toString(16)); + throw new UnsupportedOperationException("Cannot convert UInt64Type to JSON without a FieldInstance. Call " + + "the overload of this method that accepts a FieldInstance instead."); + } + + @Override + public JsonNode toJson(FieldInstance fieldInstance) { + int radix = getRadix(fieldInstance); + return new TextNode(UnsignedLong.valueOf(toHex(), 16).toString(radix).toUpperCase()); + } + + /** + * Most UInt64s are represented as hex Strings in JSON. However, some MPT related fields are represented in base 10 in + * JSON. This method determines the radix of the field based on the supplied {@link FieldInstance}'s name. + * + * @param fieldInstance A {@link FieldInstance}. + * + * @return An int representing the radix. + */ + private static int getRadix(FieldInstance fieldInstance) { + int radix = 16; + if (BASE_10_UINT64_FIELD_NAMES.contains(fieldInstance.name())) { + radix = 10; + } + return radix; } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UIntType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UIntType.java index 915dcc678..c9a5c9f6a 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UIntType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/UIntType.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.base.Preconditions; import com.google.common.primitives.UnsignedLong; import org.xrpl.xrpl4j.codec.addresses.ByteUtils; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; @@ -31,19 +32,20 @@ */ abstract class UIntType> extends SerializedType { - private final UnsignedLong value; - public UIntType(UnsignedLong value, int bitSize) { super(UnsignedByteArray.fromHex(ByteUtils.padded(value.toString(16), bitSizeToHexLength(bitSize)))); - this.value = value; } - private static int bitSizeToHexLength(int bitSize) { - return bitSize / 4; + public UIntType(UnsignedByteArray value, int bitSize) { + super(UnsignedByteArray.fromHex(ByteUtils.padded(value.hexValue(), bitSizeToHexLength(bitSize)))); + Preconditions.checkArgument( + value.length() == bitSize / 8, + String.format("Invalid %s length: %s", this.getClass().getSimpleName(), value.length()) + ); } - UnsignedLong valueOf() { - return value; + private static int bitSizeToHexLength(int bitSize) { + return bitSize / 4; } @Override diff --git a/xrpl4j-core/src/main/resources/definitions.json b/xrpl4j-core/src/main/resources/definitions.json index 797be9ce2..6e2257e8d 100644 --- a/xrpl4j-core/src/main/resources/definitions.json +++ b/xrpl4j-core/src/main/resources/definitions.json @@ -53,6 +53,8 @@ "AMM": 121, "DID": 73, "Oracle": 128, + "MPTokenIssuance": 126, + "MPToken": 127, "Any": -3, "Child": -2, "Nickname": 110, @@ -220,6 +222,16 @@ "type": "UInt8" } ], + [ + "AssetScale", + { + "nth": 5, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt8" + } + ], [ "TickSize", { @@ -370,6 +382,16 @@ "type": "UInt16" } ], + [ + "LedgerFixType", + { + "nth": 21, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt16" + } + ], [ "NetworkID", { @@ -1070,6 +1092,36 @@ "type": "UInt64" } ], + [ + "MaximumAmount", + { + "nth": 24, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "OutstandingAmount", + { + "nth": 25, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "MPTAmount", + { + "nth": 26, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], [ "EmailHash", { @@ -1120,6 +1172,16 @@ "type": "Hash160" } ], + [ + "MPTokenIssuanceID", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt192" + } + ], [ "LedgerHash", { @@ -1980,6 +2042,16 @@ "type": "Blob" } ], + [ + "MPTokenMetadata", + { + "nth": 30, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], [ "Account", { @@ -2070,6 +2142,16 @@ "type": "AccountID" } ], + [ + "Holder", + { + "nth": 11, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], [ "HookAccount", { @@ -2808,6 +2890,7 @@ "temEMPTY_DID": -254, "temARRAY_EMPTY": -253, "temARRAY_TOO_LARGE": -252, + "temBAD_TRANSFER_FEE": -251, "tefFAILURE": -199, "tefALREADY": -198, @@ -2830,6 +2913,7 @@ "tefTOO_BIG": -181, "tefNO_TICKET": -180, "tefNFTOKEN_IS_NOT_TRANSFERABLE": -179, + "tefINVALID_LEDGER_FIX_TYPE": -178, "terRETRY": -99, "terFUNDS_SPENT": -98, @@ -2923,7 +3007,8 @@ "tecINVALID_UPDATE_TIME": 188, "tecTOKEN_PAIR_NOT_FOUND": 189, "tecARRAY_EMPTY": 190, - "tecARRAY_TOO_LARGE": 191 + "tecARRAY_TOO_LARGE": 191, + "tecLOCKED": 192 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2974,8 +3059,13 @@ "DIDDelete": 50, "OracleSet": 51, "OracleDelete": 52, + "LedgerStateFix": 53, + "MPTokenIssuanceCreate": 54, + "MPTokenIssuanceDestroy": 55, + "MPTokenIssuanceSet": 56, + "MPTokenAuthorize": 57, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 } -} +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/AmountTypeTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/AmountTypeTest.java index ccdcbca1d..b51cf4b52 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/AmountTypeTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/AmountTypeTest.java @@ -21,11 +21,14 @@ */ import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertThrows; import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedLong; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.provider.Arguments; +import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser; import java.io.IOException; import java.util.stream.Stream; @@ -217,4 +220,59 @@ void encodeLargeNegativeCurrencyAmount() { assertThat(codec.fromJson(json).toHex()).isEqualTo(hex); } + @Test + void encodeDecodeMptAmount() { + String json = "{\"value\":\"100\",\"mpt_issuance_id\":\"00002403C84A0A28E0190E208E982C352BBD5006600555CF\"}"; + AmountType fromJson = codec.fromJson(json); + assertThat(fromJson.toHex()) + .isEqualTo("60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF"); + assertThat(fromJson.toJson().toString()).isEqualTo(json); + } + + @Test + void encodeDecodeMptAmountNegative() { + String json = "{\"value\":\"-100\",\"mpt_issuance_id\":\"00002403C84A0A28E0190E208E982C352BBD5006600555CF\"}"; + AmountType fromJson = codec.fromJson(json); + assertThat(fromJson.toHex()) + .isEqualTo("20000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF"); + assertThat(fromJson.toJson().toString()).isEqualTo(json); + } + + @Test + void encodeDecodeLargestAmount() { + String json = "{\"value\":\"9223372036854775807\"," + + "\"mpt_issuance_id\":\"00002403C84A0A28E0190E208E982C352BBD5006600555CF\"}"; + AmountType fromJson = codec.fromJson(json); + assertThat(fromJson.toHex()) + .isEqualTo("607FFFFFFFFFFFFFFF00002403C84A0A28E0190E208E982C352BBD5006600555CF"); + assertThat(fromJson.toJson().toString()).isEqualTo(json); + } + + @Test + void encodeDecodeLargestAmountNegative() { + String json = "{\"value\":\"-9223372036854775807\"," + + "\"mpt_issuance_id\":\"00002403C84A0A28E0190E208E982C352BBD5006600555CF\"}"; + AmountType fromJson = codec.fromJson(json); + assertThat(fromJson.toHex()) + .isEqualTo("207FFFFFFFFFFFFFFF00002403C84A0A28E0190E208E982C352BBD5006600555CF"); + assertThat(fromJson.toJson().toString()).isEqualTo(json); + } + + @Test + void encodeMptAmountWithMoreThan63BitAmountThrows() { + UnsignedLong maxLongPlusOne = UnsignedLong.valueOf(Long.MAX_VALUE).plus(UnsignedLong.ONE); + String json = "{\"value\":\"" + maxLongPlusOne + "\"," + + "\"mpt_issuance_id\":\"00002403C84A0A28E0190E208E982C352BBD5006600555CF\"}"; + assertThatThrownBy(() -> codec.fromJson(json)).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid MPT amount. Maximum MPT value is (2^63 - 1)"); + } + + @Test + void encodeMptAmountWithMoreThan63BitAmountThrowsNegative() { + UnsignedLong maxLongPlusOne = UnsignedLong.valueOf(Long.MAX_VALUE).plus(UnsignedLong.ONE); + String json = "{\"value\":\"-" + maxLongPlusOne + "\"," + + "\"mpt_issuance_id\":\"00002403C84A0A28E0190E208E982C352BBD5006600555CF\"}"; + assertThatThrownBy(() -> codec.fromJson(json)).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid MPT amount. Maximum MPT value is (2^63 - 1)"); + } } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/BaseSerializerTypeTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/BaseSerializerTypeTest.java index 12386ba06..6416aa979 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/BaseSerializerTypeTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/BaseSerializerTypeTest.java @@ -50,12 +50,18 @@ protected static Stream dataDrivenFixturesForType(SerializedType seri @ParameterizedTest @MethodSource("dataDrivenFixtures") void fixtureTests(ValueTest fixture) throws IOException { - SerializedType serializedType = getType(); + SerializedType serializedType = getType(); JsonNode value = getValue(fixture); if (fixture.error() != null) { assertThrows(Exception.class, () -> serializedType.fromJson(value)); } else { - assertThat(serializedType.fromJson(value).toHex()).isEqualTo(fixture.expectedHex()); + SerializedType serialized = serializedType.fromJson(value); + if (fixture.type().equals("Amount")) { + AmountType amountType = (AmountType) serialized; + assertThat(amountType.isPositive()).isEqualTo(!fixture.isNegative()); + assertThat(amountType.isNative()).isEqualTo(fixture.isNative()); + } + assertThat(serialized.toHex()).isEqualTo(fixture.expectedHex()); } } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/HashTypeTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/HashTypeTest.java index 1a02414cd..96cb30ad3 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/HashTypeTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/HashTypeTest.java @@ -24,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.base.Strings; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class HashTypeTest { @@ -32,12 +31,14 @@ class HashTypeTest { public static final char DOUBLE_QUOTE = '"'; private final Hash128Type codec128 = new Hash128Type(); private final Hash160Type codec160 = new Hash160Type(); + private final UInt192Type codec192 = new UInt192Type(); private final Hash256Type codec256 = new Hash256Type(); @Test void decode() { assertThat(codec128.fromHex(bytes(16)).toHex()).isEqualTo(bytes(16)); assertThat(codec160.fromHex(bytes(20)).toHex()).isEqualTo(bytes(20)); + assertThat(codec192.fromHex(bytes(24)).toHex()).isEqualTo(bytes(24)); assertThat(codec256.fromHex(bytes(32)).toHex()).isEqualTo(bytes(32)); } @@ -45,6 +46,7 @@ void decode() { void encode() { assertThat(codec128.fromJson(DOUBLE_QUOTE + bytes(16) + DOUBLE_QUOTE).toHex()).isEqualTo(bytes(16)); assertThat(codec160.fromJson(DOUBLE_QUOTE + bytes(20) + DOUBLE_QUOTE).toHex()).isEqualTo(bytes(20)); + assertThat(codec192.fromJson(DOUBLE_QUOTE + bytes(24) + DOUBLE_QUOTE).toHex()).isEqualTo(bytes(24)); assertThat(codec256.fromJson(DOUBLE_QUOTE + bytes(32) + DOUBLE_QUOTE).toHex()).isEqualTo(bytes(32)); } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt16TypeUnitTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt16TypeUnitTest.java index ab35eb038..10149b65f 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt16TypeUnitTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt16TypeUnitTest.java @@ -33,9 +33,9 @@ class UInt16TypeUnitTest { @Test void decode() { - assertThat(codec.fromHex("0000").valueOf()).isEqualTo(UnsignedLong.valueOf(0)); - assertThat(codec.fromHex("000F").valueOf()).isEqualTo(UnsignedLong.valueOf(15)); - assertThat(codec.fromHex("FFFF").valueOf()).isEqualTo(UnsignedLong.valueOf(65535)); + assertThat(codec.fromHex("0000").toHex()).isEqualTo("0000"); + assertThat(codec.fromHex("000F").toHex()).isEqualTo("000F"); + assertThat(codec.fromHex("FFFF").toHex()).isEqualTo("FFFF"); } @Test diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt32TypeUnitTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt32TypeUnitTest.java index b550d2bda..80813cd96 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt32TypeUnitTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt32TypeUnitTest.java @@ -33,9 +33,9 @@ public class UInt32TypeUnitTest { @Test void decode() { - assertThat(codec.fromHex("00000000").valueOf()).isEqualTo(UnsignedLong.valueOf(0)); - assertThat(codec.fromHex("0000000F").valueOf()).isEqualTo(UnsignedLong.valueOf(15)); - assertThat(codec.fromHex("FFFFFFFF").valueOf()).isEqualTo(UnsignedLong.valueOf(4294967295L)); + assertThat(codec.fromHex("00000000").toHex()).isEqualTo("00000000"); + assertThat(codec.fromHex("0000000F").toHex()).isEqualTo("0000000F"); + assertThat(codec.fromHex("FFFFFFFF").toHex()).isEqualTo("FFFFFFFF"); } @Test diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt64TypeUnitTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt64TypeUnitTest.java index e162389b0..48608198f 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt64TypeUnitTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/UInt64TypeUnitTest.java @@ -21,35 +21,97 @@ */ import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; +import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; +import org.xrpl.xrpl4j.codec.binary.definitions.FieldInstance; + +import java.util.stream.Stream; public class UInt64TypeUnitTest { - private final UInt64Type codec = new UInt64Type(); - private static UnsignedLong maxUint64 = UnsignedLong.valueOf("FFFFFFFFFFFFFFFF", 16); + private final UInt64Type type = new UInt64Type(); + + @Test + void testFromHex() { + assertThat(type.fromHex("0000000000000000").toHex()).isEqualTo("0000000000000000"); + assertThat(type.fromHex("000000000000000F").toHex()).isEqualTo("000000000000000F"); + assertThat(type.fromHex("00000000FFFFFFFF").toHex()).isEqualTo("00000000FFFFFFFF"); + assertThat(type.fromHex("FFFFFFFFFFFFFFFF").toHex()).isEqualTo("FFFFFFFFFFFFFFFF"); + } + + @ParameterizedTest + @MethodSource(value = "base16JsonArguments") + void testFromJsonBase16(TextNode json) { + FieldInstance base16FieldInstance = mock(FieldInstance.class); + when(base16FieldInstance.name()).thenReturn("Base16Field"); + assertThat(type.fromJson(json, base16FieldInstance).toHex()) + .isEqualTo(Strings.padStart(json.asText(), 16, '0')); + assertThat(type.fromJson(json, base16FieldInstance).toJson(base16FieldInstance)).isEqualTo(json); + } + + @ParameterizedTest + @MethodSource(value = "base10JsonArguments") + void testFromJsonBase10(TextNode json) { + UInt64Type.BASE_10_UINT64_FIELD_NAMES.forEach( + b10FieldName -> { + FieldInstance base10FieldInstance = mock(FieldInstance.class); + when(base10FieldInstance.name()).thenReturn(b10FieldName); + String expectedHex = Strings.padStart(UnsignedLong.valueOf(json.asText()).toString(16).toUpperCase(), 16, '0'); + assertThat(type.fromJson(json, base10FieldInstance).toHex()) + .isEqualTo(expectedHex); + assertThat(type.fromJson(json, base10FieldInstance).toJson(base10FieldInstance)).isEqualTo(json); + } + ); + } @Test - void decode() { - assertThat(codec.fromHex("0000000000000000").valueOf()).isEqualTo(UnsignedLong.valueOf(0)); - assertThat(codec.fromHex("000000000000000F").valueOf()).isEqualTo(UnsignedLong.valueOf(15)); - assertThat(codec.fromHex("00000000FFFFFFFF").valueOf()).isEqualTo(UnsignedLong.valueOf(4294967295L)); - assertThat(codec.fromHex("FFFFFFFFFFFFFFFF").valueOf()).isEqualTo(maxUint64); + void fromJsonThrowsWithoutFieldInstance() { + assertThatThrownBy(() -> type.fromJson(new TextNode("0"))) + .isInstanceOf(UnsupportedOperationException.class); } @Test - void encode() { - assertThat(codec.fromJson("\"0\"").toHex()).isEqualTo("0000000000000000"); - assertThat(codec.fromJson("\"F\"").toHex()).isEqualTo("000000000000000F"); - assertThat(codec.fromJson("\"FFFF\"").toHex()).isEqualTo("000000000000FFFF"); - assertThat(codec.fromJson("\"FFFFFFFF\"").toHex()).isEqualTo("00000000FFFFFFFF"); - assertThat(codec.fromJson("\"FFFFFFFFFFFFFFFF\"").toHex()).isEqualTo("FFFFFFFFFFFFFFFF"); + void toJsonThrowsWithoutFieldInstance() { + assertThatThrownBy(type::toJson) + .isInstanceOf(UnsupportedOperationException.class); + } + + private static Stream base16JsonArguments() { + return Stream.of( + new TextNode("0"), + new TextNode("F"), + new TextNode("FFFF"), + new TextNode("FFFFFFFF"), + new TextNode("FFFFFFFFFFFFFFFF") + ); + } + + private static Stream base10JsonArguments() { + return Stream.of( + new TextNode("0"), + new TextNode("15"), + new TextNode("65535"), + new TextNode("4294967295"), + new TextNode("18446744073709551615") + ); } @Test void encodeOutOfBounds() { - assertThrows(IllegalArgumentException.class, () -> codec.fromJson("18446744073709551616")); + FieldInstance field = mock(FieldInstance.class); + when(field.name()).thenReturn("Field"); + assertThrows(IllegalArgumentException.class, () -> type.fromJson(new TextNode("18446744073709551616"), field)); } } diff --git a/xrpl4j-core/src/test/resources/data-driven-tests.json b/xrpl4j-core/src/test/resources/data-driven-tests.json index 5e82b5a00..745eb7a28 100644 --- a/xrpl4j-core/src/test/resources/data-driven-tests.json +++ b/xrpl4j-core/src/test/resources/data-driven-tests.json @@ -2524,21 +2524,6 @@ "is_negative": false, "exponent": -15 }, - { - "test_json": { - "currency": "USD", - "value": "0", - "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji" - }, - "significant_digits": 1, - "type_id": 6, - "is_native": false, - "mantissa": "0000000000000000", - "type": "Amount", - "expected_hex": "800000000000000000000000000000000000000055534400000000000000000000000000000000000000000000000001", - "is_negative": false, - "exponent": -15 - }, { "test_json": { "currency": "USD", @@ -2569,21 +2554,6 @@ "is_negative": false, "exponent": -15 }, - { - "test_json": { - "currency": "USD", - "value": "0.0", - "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji" - }, - "significant_digits": 1, - "type_id": 6, - "is_native": false, - "mantissa": "0000000000000000", - "type": "Amount", - "expected_hex": "800000000000000000000000000000000000000055534400000000000000000000000000000000000000000000000001", - "is_negative": false, - "exponent": -15 - }, { "test_json": { "currency": "USD", @@ -3679,6 +3649,150 @@ "type_specialisation_field": "TransactionResult", "type": "UInt8", "expected_hex": "8D" + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "9223372036854775808" + }, + "type": "Amount", + "error": "Value is too large" + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "18446744073709551615" + }, + "type": "Amount", + "error": "Value is too large" + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "-1" + }, + "is_native": false, + "type": "Amount", + "expected_hex": "20000000000000000100002403C84A0A28E0190E208E982C352BBD5006600555CF", + "is_negative": true + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "10.1" + }, + "type": "Amount", + "error": "Value has decimal point" + }, + { + "test_json": { + "mpt_issuance_id": "10", + "value": "10" + }, + "type": "Amount", + "error": "mpt_issuance_id has invalid hash length" + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "10", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji" + }, + "type": "Amount", + "error": "Issuer not valid for MPT" + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "10", + "currency": "USD" + }, + "type": "Amount", + "error": "Currency not valid for MPT" + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "a" + }, + "type": "Amount", + "error": "Value has incorrect hex format" + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "0xy" + }, + "type": "Amount", + "error": "Value has bad hex character" + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "/" + }, + "type": "Amount", + "error": "Value has bad character" + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "0x8000000000000000" + }, + "type": "Amount", + "error": "Hex value out of range" + }, + { + "test_json": { + "mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "0xFFFFFFFFFFFFFFFF" + }, + "type": "Amount", + "error": "Hex value out of range" + }, + { + "test_json": { + "mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "9223372036854775807" + }, + "type_id": 6, + "is_native": false, + "type": "Amount", + "expected_hex": "607FFFFFFFFFFFFFFF00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "is_negative": false + }, + { + "test_json": { + "mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "0" + }, + "type_id": 6, + "is_native": false, + "type": "Amount", + "expected_hex": "60000000000000000000002403C84A0A28E0190E208E982C352BBD5006600555CF", + "is_negative": false + }, + { + "test_json": { + "mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "-0" + }, + "type_id": 6, + "is_native": false, + "type": "Amount", + "expected_hex": "20000000000000000000002403C84A0A28E0190E208E982C352BBD5006600555CF", + "is_negative": true + }, + { + "test_json": { + "mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF", + "value": "100" + }, + "type_id": 6, + "is_native": false, + "type": "Amount", + "expected_hex": "60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF", + "is_negative": false } ] }