Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add binary codec support for MPT amounts and STHash192 #556

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class AmountType extends SerializedType<AmountType> {
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;

/**
Expand Down Expand Up @@ -142,14 +143,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("-");
Expand All @@ -166,22 +176,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) {
Expand Down Expand Up @@ -213,7 +247,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);
Expand Down Expand Up @@ -251,8 +301,15 @@ 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;
// 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;
}

private 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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
sappenin marked this conversation as resolved.
Show resolved Hide resolved

@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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -48,6 +49,7 @@ public abstract class SerializedType<T extends SerializedType<T>> {
.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())
Expand Down Expand Up @@ -122,10 +124,27 @@ public T fromParser(BinaryParser parser, int lengthHint) {
* @param node A {@link JsonNode} to use.
*
* @return A {@link T} based upon the information found in {@code node}.
*
nkramer44 marked this conversation as resolved.
Show resolved Hide resolved
* @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}.
*
Expand Down Expand Up @@ -197,6 +216,19 @@ public JsonNode toJson() {
return new TextNode(toHex());
nkramer44 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UInt192Type> {

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()));
}
}
Loading
Loading