Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult;
import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams;
import org.xrpl.xrpl4j.model.client.ledger.LedgerResult;
import org.xrpl.xrpl4j.model.client.mpt.MptHoldersRequestParams;
import org.xrpl.xrpl4j.model.client.mpt.MptHoldersResponse;
import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersRequestParams;
import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersResult;
import org.xrpl.xrpl4j.model.client.nft.NftInfoRequestParams;
Expand Down Expand Up @@ -873,6 +875,24 @@ public GetAggregatePriceResult getAggregatePrice(
return jsonRpcClient.send(request, GetAggregatePriceResult.class);
}

/**
* Get all holders of an MPT and their balance. The mpt_holders method is only available on Clio nodes.
*
* @param params An {@link MptHoldersRequestParams}.
*
* @return An {@link MptHoldersResponse}.
*
* @throws JsonRpcClientErrorException if {@code js nRpcClient} throws an error.
*/
public MptHoldersResponse mptHolders(MptHoldersRequestParams params) throws JsonRpcClientErrorException {
JsonRpcRequest request = JsonRpcRequest.builder()
.method(XrplMethods.MPT_HOLDERS)
.addParams(params)
.build();

return jsonRpcClient.send(request, MptHoldersResponse.class);
}

public JsonRpcClient getJsonRpcClient() {
return jsonRpcClient;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult;
import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams;
import org.xrpl.xrpl4j.model.client.ledger.LedgerResult;
import org.xrpl.xrpl4j.model.client.mpt.MptHoldersRequestParams;
import org.xrpl.xrpl4j.model.client.mpt.MptHoldersResponse;
import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersRequestParams;
import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersResult;
import org.xrpl.xrpl4j.model.client.nft.NftInfoRequestParams;
Expand Down Expand Up @@ -1121,4 +1123,22 @@ void getAggregatePrice() throws JsonRpcClientErrorException {

assertThat(result).isEqualTo(expectedResult);
}

@Test
void mptHolders() throws JsonRpcClientErrorException {
MptHoldersRequestParams params = mock(MptHoldersRequestParams.class);
MptHoldersResponse expectedResult = mock(MptHoldersResponse.class);

when(jsonRpcClientMock.send(
JsonRpcRequest.builder()
.method(XrplMethods.MPT_HOLDERS)
.addParams(params)
.build(),
MptHoldersResponse.class
)).thenReturn(expectedResult);

MptHoldersResponse result = xrplClient.mptHolders(params);

assertThat(result).isEqualTo(expectedResult);
}
}
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 All @@ -121,7 +117,7 @@ public void writeFieldAndValue(final FieldInstance field, final JsonNode value)
*
* @param value length encoded value to write to BytesList.
*/
public void writeLengthEncoded(final SerializedType value) {
public void writeLengthEncoded(final SerializedType<?> value) {
Objects.requireNonNull(value);
UnsignedByteArray bytes = UnsignedByteArray.empty();
value.toBytesSink(bytes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import org.xrpl.xrpl4j.codec.binary.math.MathUtils;
import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser;
import org.xrpl.xrpl4j.model.immutables.FluentCompareTo;
import org.xrpl.xrpl4j.model.transactions.MpTokenIssuanceId;
import org.xrpl.xrpl4j.model.transactions.MptCurrencyAmount;

import java.math.BigDecimal;
import java.math.BigInteger;
Expand All @@ -50,6 +52,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 +145,28 @@ 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();
// The first bit is 0 for XRP or MPT, and 1 for IOU.
boolean isIssuedCurrency = nextByte.isNthBitSet(1);

int numBytes;
if (isIssuedCurrency) {
numBytes = CURRENCY_AMOUNT_BYTE_LENGTH;
} else {
// The third bit is 1 for MPT, and 0 for XRP
boolean isMpt = nextByte.isNthBitSet(3);

numBytes = isMpt ? MPT_AMOUNT_BYTE_LENGTH : NATIVE_AMOUNT_BYTE_LENGTH;
}

// parse all bytes, including the token-type bytes peeked above.
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 +183,48 @@ 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();

UnsignedByteArray currency = new CurrencyType().fromJson(value.get("currency")).value();
UnsignedByteArray issuer = new AccountIdType().fromJson(value.get("issuer")).value();
result.append(currency);
result.append(issuer);

result.append(currency);
result.append(issuer);
return new AmountType(result);
} else {
// MPT Amount
MptCurrencyAmount mptCurrencyAmount = objectMapper.treeToValue(value, MptCurrencyAmount.class);

if (FluentCompareTo.is(mptCurrencyAmount.unsignedLongValue()).greaterThan(UnsignedLong.valueOf(Long.MAX_VALUE))) {
throw new IllegalArgumentException("Invalid MPT mptCurrencyAmount. Maximum MPT value is (2^63 - 1)");
}

return new AmountType(result);
UnsignedByteArray amountBytes = UnsignedByteArray.fromHex(
ByteUtils.padded(
mptCurrencyAmount.unsignedLongValue().toString(16),
16 // <-- 64 / 4
)
);
UnsignedByteArray issuanceIdBytes = new UInt192Type()
.fromJson(new TextNode(mptCurrencyAmount.mptIssuanceId().value()))
.value();

// MPT Amounts always have 0110_000 (0x60) as the first byte when positive or 0010_0000 (0x20) when negative.
int leadingByte = mptCurrencyAmount.isNegative() ? 0x20 : 0x60;
UnsignedByteArray result = UnsignedByteArray.of(UnsignedByte.of(leadingByte));
result.append(amountBytes);
result.append(issuanceIdBytes);

return new AmountType(result);
}
}

private UnsignedByteArray getAmountBytes(BigDecimal number) {
Expand Down Expand Up @@ -213,7 +256,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);
MptCurrencyAmount mptAmount = MptCurrencyAmount.builder()
.value(isNegative ? "-" + amountBase10 : amountBase10)
.mptIssuanceId(MpTokenIssuanceId.of(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 @@ -250,17 +309,24 @@ 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;
}

/**
* Determines if this AmountType is positive.
*
* @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;
}
Expand Down
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 @@
.put("Currency", () -> new CurrencyType())
.put("Hash128", () -> new Hash128Type())
.put("Hash160", () -> new Hash160Type())
.put("Hash192", () -> new UInt192Type())
.put("Hash256", () -> new Hash256Type())
.put("PathSet", () -> new PathSetType())
.put("STArray", () -> new STArrayType())
Expand All @@ -74,7 +76,11 @@
* @return A {@link SerializedType} for the supplied {@code name}.
*/
public static SerializedType<?> getTypeByName(String name) {
return typeMap.get(name).get();
try {
return typeMap.get(name).get();
} catch (NullPointerException e) {
throw e;

Check warning on line 82 in xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/SerializedType.java

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L81 - L82 were not covered by tests
}
}

/**
Expand Down Expand Up @@ -117,15 +123,34 @@
}

/**
* 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}.
*
Expand Down Expand Up @@ -189,14 +214,29 @@
}

/**
* 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}.
*/
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}.
*
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