diff --git a/README.md b/README.md index 8e1bcade2..60c613d2d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ current [BOM](https://howtodoinjava.com/maven/maven-bom-bill-of-materials-depend org.xrpl xrpl4j-bom - 3.0.1 + 3.2.1 pom import @@ -141,17 +141,17 @@ For use-cases that require private keys to exist inside the running JVM, the fol generate a keypair, and also how to derive an XRPL address from there: ```java -import org.xrpl.xrpl4j.crypto.core.keys.Seed; -import org.xrpl.xrpl4j.crypto.core.keys.PrivateKey; -import org.xrpl.xrpl4j.crypto.core.keys.PublicKey; +import org.xrpl.xrpl4j.crypto.keys.KeyPair; +import org.xrpl.xrpl4j.crypto.keys.PrivateKey; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.crypto.keys.Seed; import org.xrpl.xrpl4j.model.transactions.Address; - -... -Seed seed = Seed.ed255519Seed(); // <-- Generates a random seed. -PrivateKey privateKey = seed.derivePrivateKey(); // <-- Derive a private key from the seed. -PublicKey publicKey = privateKey.derivePublicKey(); // <-- Derive a public key from the private key. -Address address = publicKey.deriveAddress(); // <-- Derive an address from the public key. +Seed seed = Seed.ed25519Seed(); // <-- Generates a random seed. +KeyPair keyPair = seed.deriveKeyPair(); // <-- Derive a KeyPair from the seed. +PrivateKey privateKey = keyPair.privateKey(); // <-- Derive a privateKey from the KeyPair. +PublicKey publicKey = keyPair.publicKey(); // <-- Derive a publicKey from the KeyPair. +Address address = publicKey.deriveAddress(); // <-- Derive an address from the publicKey ``` #### Private Key References (`PrivateKeyReference`) @@ -183,23 +183,24 @@ The following example illustrates how to construct a payment transaction, sign i then submit that transaction to the XRP Ledger for processing and validation: ```java -import org.xrpl.xrpl4j.crypto.core.keys.Seed; -import org.xrpl.xrpl4j.crypto.core.keys.KeyPair; -import org.xrpl.xrpl4j.crypto.core.keys.PrivateKey; -import org.xrpl.xrpl4j.crypto.core.keys.PublicKey; +import org.xrpl.xrpl4j.client.XrplClient; +import org.xrpl.xrpl4j.crypto.keys.PrivateKey; +import org.xrpl.xrpl4j.crypto.keys.Seed; +import org.xrpl.xrpl4j.crypto.signing.SignatureService; +import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; +import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; import org.xrpl.xrpl4j.model.transactions.Address; -import org.xrpl.xrpl4j.crypto.core.signing.SignatureService; + import org.xrpl.xrpl4j.crypto.signing.bc.BcSignatureService; +import org.xrpl.xrpl4j.model.transactions.Payment; // Construct a SignatureService that uses in-memory Keys (see SignatureService.java for alternatives). SignatureService signatureService = new BcSignatureService(); // Sender (using ed25519 key) -Seed senderSeed = Seed.ed255519Seed(); -PrivateKey senderPrivateKey = senderSeed.derivePrivateKey(); -PublicKey senderPublicKey = senderPrivateKey.derivePublicKey(); -Address senderAddress = senderPublicKey.deriveAddress(); - +Seed seed = Seed.ed25519Seed(); // <-- Generates a random seed. +PrivateKey senderPrivateKey = seed.deriveKeyPair().privateKey(); + // Receiver (using secp256k1 key) Address receiverAddress = Address.of("r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59"); @@ -208,7 +209,7 @@ Payment payment = ...; // See V3 ITs for examples. SingleSignedTransaction signedTransaction = signatureService.sign(sourcePrivateKey,payment); SubmitResult result = xrplClient.submit(signedTransaction); -assertThat(result.result()).isEqualTo("tesSUCCESS"); +assert result.engineResult().equals("tesSUCCESS"); ``` ### Codecs @@ -216,7 +217,7 @@ This library relies upon two important sub-modules called Codecs (One for the XR canonical JSON encoding). Read more about each here: - [Binary Codec](https://github.com/XRPLF/xrpl4j/tree/main/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/README.md) -- [Address Coded](https://github.com/XRPLF/xrpl4j/tree/main/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/README.md) +- [Address Codec](https://github.com/XRPLF/xrpl4j/tree/main/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/README.md) ## Development diff --git a/pom.xml b/pom.xml index 43d369dbe..4a127e359 100644 --- a/pom.xml +++ b/pom.xml @@ -411,7 +411,8 @@ 3.3.0 true - UTF-8 + UTF-8 + UTF-8 true false true diff --git a/xrpl4j-bom/README.md b/xrpl4j-bom/README.md index 26f422365..7d0024a57 100644 --- a/xrpl4j-bom/README.md +++ b/xrpl4j-bom/README.md @@ -20,7 +20,7 @@ POM file. For example: org.xrpl.xrpl4j xrpl4j-bom - 3.0.0 + 3.2.1 pom import @@ -31,7 +31,7 @@ POM file. For example: With this in place, whenever you want to add a `` you won't need to worry about specifying the version. Instead, version numbers are controlled by the BOM you import, as in the example above, which will use only -version `3.0.0` of all xrpl-4j dependencies. +version `3.2.1` of all xrpl-4j dependencies. For more information on how BOM files work, consult this [tutorial](https://www.baeldung.com/spring-maven-bom) or others on Google. diff --git a/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java b/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java index a0c7d04f9..4be150a83 100644 --- a/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java +++ b/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java @@ -55,11 +55,15 @@ import org.xrpl.xrpl4j.model.client.accounts.AccountTransactionsResult; import org.xrpl.xrpl4j.model.client.accounts.GatewayBalancesRequestParams; import org.xrpl.xrpl4j.model.client.accounts.GatewayBalancesResult; +import org.xrpl.xrpl4j.model.client.amm.AmmInfoRequestParams; +import org.xrpl.xrpl4j.model.client.amm.AmmInfoResult; import org.xrpl.xrpl4j.model.client.channels.ChannelVerifyRequestParams; import org.xrpl.xrpl4j.model.client.channels.ChannelVerifyResult; import org.xrpl.xrpl4j.model.client.common.LedgerIndex; import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +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.nft.NftBuyOffersRequestParams; @@ -87,6 +91,7 @@ import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; import org.xrpl.xrpl4j.model.immutables.FluentCompareTo; import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.transactions.Address; import org.xrpl.xrpl4j.model.transactions.Hash256; import org.xrpl.xrpl4j.model.transactions.Transaction; @@ -670,6 +675,28 @@ public LedgerResult ledger(LedgerRequestParams params) throws JsonRpcClientError return jsonRpcClient.send(request, LedgerResult.class); } + /** + * Retrieve a {@link LedgerObject} by sending a {@code ledger_entry} RPC request. + * + * @param params A {@link LedgerEntryRequestParams} containing the request parameters. + * @param The type of {@link LedgerObject} that should be returned in rippled's response. + * + * @return A {@link LedgerEntryResult} of type {@link T}. + */ + public LedgerEntryResult ledgerEntry( + LedgerEntryRequestParams params + ) throws JsonRpcClientErrorException { + JsonRpcRequest request = JsonRpcRequest.builder() + .method(XrplMethods.LEDGER_ENTRY) + .addParams(params) + .build(); + + JavaType resultType = objectMapper.getTypeFactory() + .constructParametricType(LedgerEntryResult.class, params.ledgerObjectClass()); + + return jsonRpcClient.send(request, resultType); + } + /** * Try to find a payment path for a rippling payment by sending a ripple_path_find method request. * @@ -762,6 +789,27 @@ public GatewayBalancesResult gatewayBalances( return jsonRpcClient.send(request, GatewayBalancesResult.class); } + /** + * Get info about an AMM by making a call to the amm_info rippled RPC method. + * + * @param params The {@link AmmInfoRequestParams} to send in the request. + * + * @return A {@link AmmInfoResult}. + * + * @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error. + */ + @Beta + public AmmInfoResult ammInfo( + AmmInfoRequestParams params + ) throws JsonRpcClientErrorException { + JsonRpcRequest request = JsonRpcRequest.builder() + .method(XrplMethods.AMM_INFO) + .addParams(params) + .build(); + + return jsonRpcClient.send(request, AmmInfoResult.class); + } + public JsonRpcClient getJsonRpcClient() { return jsonRpcClient; } diff --git a/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java b/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java index 728ac8640..f07094164 100644 --- a/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java +++ b/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java @@ -70,11 +70,15 @@ import org.xrpl.xrpl4j.model.client.accounts.AccountTransactionsResult; import org.xrpl.xrpl4j.model.client.accounts.GatewayBalancesRequestParams; import org.xrpl.xrpl4j.model.client.accounts.GatewayBalancesResult; +import org.xrpl.xrpl4j.model.client.amm.AmmInfoRequestParams; +import org.xrpl.xrpl4j.model.client.amm.AmmInfoResult; import org.xrpl.xrpl4j.model.client.channels.ChannelVerifyRequestParams; import org.xrpl.xrpl4j.model.client.channels.ChannelVerifyResult; import org.xrpl.xrpl4j.model.client.common.LedgerIndex; import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +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.nft.NftBuyOffersRequestParams; @@ -102,8 +106,10 @@ import org.xrpl.xrpl4j.model.client.transactions.TransactionRequestParams; import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; import org.xrpl.xrpl4j.model.flags.AccountRootFlags; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; import org.xrpl.xrpl4j.model.ledger.AccountRootObject; import org.xrpl.xrpl4j.model.ledger.Issue; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.transactions.Address; import org.xrpl.xrpl4j.model.transactions.Hash256; import org.xrpl.xrpl4j.model.transactions.NfTokenId; @@ -1025,6 +1031,42 @@ public void nftSellOffers() throws JsonRpcClientErrorException { assertThat(jsonRpcRequestArgumentCaptor.getValue().params().get(0)).isEqualTo(nftSellOffersRequestParams); } + @Test + void ammInfo() throws JsonRpcClientErrorException { + AmmInfoRequestParams params = mock(AmmInfoRequestParams.class); + JsonRpcRequest expectedRequest = JsonRpcRequest.builder() + .method(XrplMethods.AMM_INFO) + .addParams(params) + .build(); + AmmInfoResult mockResult = mock(AmmInfoResult.class); + when(jsonRpcClientMock.send(expectedRequest, AmmInfoResult.class)).thenReturn(mockResult); + AmmInfoResult result = xrplClient.ammInfo(params); + + assertThat(result).isEqualTo(mockResult); + } + + @Test + void ledgerEntry() throws JsonRpcClientErrorException { + LedgerEntryRequestParams params = LedgerEntryRequestParams.index( + Hash256.of("6B1011EF3BC3ED619B15979EF75C1C60D9181F3DDE641AD3019318D3900CEE2E"), + LedgerSpecifier.VALIDATED + ); + + LedgerEntryResult mockResult = mock(LedgerEntryResult.class); + when(jsonRpcClientMock.send( + JsonRpcRequest.builder() + .method(XrplMethods.LEDGER_ENTRY) + .addParams(params) + .build(), + ObjectMapperFactory.create().getTypeFactory().constructParametricType( + LedgerEntryResult.class, LedgerObject.class + ) + )).thenReturn(mockResult); + + LedgerEntryResult result = xrplClient.ledgerEntry(params); + assertThat(result).isEqualTo(mockResult); + } + @Test void nftInfo() throws JsonRpcClientErrorException { NftInfoRequestParams params = NftInfoRequestParams.builder() diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/AddressBase58.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/AddressBase58.java index f070af1e7..a4481a5a8 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/AddressBase58.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/AddressBase58.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. @@ -56,7 +56,7 @@ public static String encode( Objects.requireNonNull(expectedLength); if (expectedLength.intValue() != bytes.getUnsignedBytes().size()) { - throw new EncodeException("Length of bytes does not match expectedLength."); + throw new EncodeException(String.format("Length of bytes does not match expectedLength of %s.", expectedLength)); } return encodeChecked(bytes.toByteArray(), versions); @@ -99,6 +99,7 @@ public static String encodeChecked(final byte[] bytes, final List versi * @param version The {@link Version} to try decoding with. * * @return A {@link Decoded} containing the decoded value and version. + * * @throws EncodingFormatException If the version bytes of the Base58 value are invalid. */ public static Decoded decode( @@ -136,14 +137,14 @@ public static Decoded decode( * Decode a Base58Check {@link String}. * * @param base58Value The Base58Check encoded {@link String} to be decoded. - * @param keyTypes A {@link List} of {@link KeyType}s which can be associated with the result of this - * method. + * @param keyTypes A {@link List} of {@link KeyType}s which can be associated with the result of this method. * @param versions A {@link List} of {@link Version}s to try decoding with. * @param expectedLength The expected length of the decoded value. * * @return A {@link Decoded} containing the decoded value, version, and type. - * @throws EncodingFormatException If more than one version is supplied without an expectedLength value present, - * or if the version bytes of the Base58 value are invalid. + * + * @throws EncodingFormatException If more than one version is supplied without an expectedLength value present, or if + * the version bytes of the Base58 value are invalid. */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public static Decoded decode( diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/AddressCodec.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/AddressCodec.java index b7ea342d3..941d14911 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/AddressCodec.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/AddressCodec.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. @@ -86,7 +86,10 @@ public UnsignedByteArray decodeAccountId(final Address accountId) { * @param publicKey An {@link UnsignedByteArray} containing the public key to be encoded. * * @return The Base58 representation of publicKey. + * + * @deprecated Prefer {@link PublicKeyCodec#encodeNodePublicKey(UnsignedByteArray)}. */ + @Deprecated public String encodeNodePublicKey(final UnsignedByteArray publicKey) { Objects.requireNonNull(publicKey); @@ -101,7 +104,9 @@ public String encodeNodePublicKey(final UnsignedByteArray publicKey) { * @return An {@link UnsignedByteArray} containing the decoded public key. * * @see "https://xrpl.org/base58-encodings.html" + * @deprecated Prefer {@link PublicKeyCodec#decodeNodePublicKey(String)}. */ + @Deprecated public UnsignedByteArray decodeNodePublicKey(final String publicKey) { Objects.requireNonNull(publicKey); diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/PrivateKeyCodec.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/PrivateKeyCodec.java new file mode 100644 index 000000000..0dd645c90 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/PrivateKeyCodec.java @@ -0,0 +1,111 @@ +package org.xrpl.xrpl4j.codec.addresses; + +/*- + * ========================LICENSE_START================================= + * xrpl4j :: core + * %% + * Copyright (C) 2020 - 2023 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.google.common.collect.Lists; +import com.google.common.primitives.UnsignedInteger; + +import java.util.Objects; + +/** + * A Codec for encoding/decoding various seed primitives. + */ +public class PrivateKeyCodec { + + private static final PrivateKeyCodec INSTANCE = new PrivateKeyCodec(); + + public static PrivateKeyCodec getInstance() { + return INSTANCE; + } + + /** + * Encode an XRPL Node Private Key to a Base58Check encoded {@link String}. + * + * @param privateKeyBytes An {@link UnsignedByteArray} containing the public key to be encoded. + * + * @return The Base58 representation of privateKeyBytes. + */ + public String encodeNodePrivateKey(final UnsignedByteArray privateKeyBytes) { + Objects.requireNonNull(privateKeyBytes); + + return AddressBase58.encode( + privateKeyBytes, + Lists.newArrayList(Version.NODE_PRIVATE), + UnsignedInteger.valueOf(32) + ); + } + + /** + * Decode a Base58Check encoded XRPL Node Private Key. + * + * @param privateKeyBase58 The Base58 encoded public key to be decoded. + * + * @return An {@link UnsignedByteArray} containing the decoded public key. + * + * @see "https://xrpl.org/base58-encodings.html" + */ + public UnsignedByteArray decodeNodePrivateKey(final String privateKeyBase58) { + Objects.requireNonNull(privateKeyBase58); + + return AddressBase58.decode( + privateKeyBase58, + Lists.newArrayList(Version.NODE_PRIVATE), + UnsignedInteger.valueOf(32) + ).bytes(); + } + + /** + * Encode an XRPL Account Private Key to a Base58Check encoded {@link String}. + * + * @param privateKeyBytes An {@link UnsignedByteArray} containing the public key to be encoded. + * + * @return The Base58 representation of privateKeyBytes. + */ + public String encodeAccountPrivateKey(final UnsignedByteArray privateKeyBytes) { + Objects.requireNonNull(privateKeyBytes); + + return AddressBase58.encode( + privateKeyBytes, + Lists.newArrayList(Version.ACCOUNT_SECRET_KEY), + UnsignedInteger.valueOf(32) + ); + } + + /** + * Decode a Base58Check encoded XRPL Account Private Key. + * + * @param privateKeyBase58 The Base58 encoded public key to be decoded. + * + * @return An {@link UnsignedByteArray} containing the decoded public key. + * + * @see "https://xrpl.org/base58-encodings.html" + */ + public UnsignedByteArray decodeAccountPrivateKey(final String privateKeyBase58) { + Objects.requireNonNull(privateKeyBase58); + + return AddressBase58.decode( + privateKeyBase58, + Lists.newArrayList(Version.ACCOUNT_SECRET_KEY), + UnsignedInteger.valueOf(32) + ).bytes(); + } + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/PublicKeyCodec.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/PublicKeyCodec.java index 9bb369ee2..0dff88cfa 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/PublicKeyCodec.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/PublicKeyCodec.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. @@ -28,7 +28,6 @@ /** * A Codec for encoding/decoding various seed primitives. */ -@SuppressWarnings( {"OptionalUsedAsFieldOrParameterType", "ParameterName", "MethodName"}) public class PublicKeyCodec { private static final PublicKeyCodec INSTANCE = new PublicKeyCodec(); @@ -46,7 +45,12 @@ public static PublicKeyCodec getInstance() { */ public String encodeNodePublicKey(final UnsignedByteArray publicKey) { Objects.requireNonNull(publicKey); - return AddressBase58.encode(publicKey, Lists.newArrayList(Version.NODE_PUBLIC), UnsignedInteger.valueOf(33)); + + return AddressBase58.encode( + publicKey, + Lists.newArrayList(Version.NODE_PUBLIC), + UnsignedInteger.valueOf(33) + ); } /** @@ -78,7 +82,11 @@ public UnsignedByteArray decodeNodePublicKey(final String publicKey) { public String encodeAccountPublicKey(final UnsignedByteArray publicKey) { Objects.requireNonNull(publicKey); - return AddressBase58.encode(publicKey, Lists.newArrayList(Version.ACCOUNT_PUBLIC_KEY), UnsignedInteger.valueOf(33)); + return AddressBase58.encode( + publicKey, + Lists.newArrayList(Version.ACCOUNT_PUBLIC_KEY), + UnsignedInteger.valueOf(33) + ); } /** diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByte.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByte.java index ad6528bf1..781bbe702 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByte.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByte.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. @@ -42,6 +42,17 @@ private UnsignedByte(final int value) { this.value = value; } + /** + * Copy constructor. Constructs an {@link UnsignedByte} from an {@code UnsignedByte}. + * + * @param value An {@code int} value. + * + * @return An {@link UnsignedByte}. + */ + public static UnsignedByte of(final UnsignedByte value) { + return new UnsignedByte(value.asInt()); + } + /** * Construct an {@link UnsignedByte} from an {@code int}. * @@ -141,8 +152,8 @@ public boolean isNthBitSet(final int nth) { } /** - * Does a bitwise OR on this {@link UnsignedByte} and the given {@link UnsignedByte}, and returns a new {@link - * UnsignedByte} as the result. + * Does a bitwise OR on this {@link UnsignedByte} and the given {@link UnsignedByte}, and returns a new + * {@link UnsignedByte} as the result. * * @param unsignedByte The {@link UnsignedByte} to perform a bitwise OR on. * diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArray.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArray.java index 525c55ed6..096565095 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArray.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArray.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. diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/Version.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/Version.java index 25c828edb..48558b2db 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/Version.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/Version.java @@ -23,10 +23,12 @@ public enum Version { ED25519_SEED(new int[] {0x01, 0xE1, 0x4B}), - FAMILY_SEED(new int[] {0x21}), + FAMILY_SEED(new int[] {0x21}), // 33 in decimal ACCOUNT_ID(new int[] {0}), - NODE_PUBLIC(new int[] {0x1C}), - ACCOUNT_PUBLIC_KEY(new int[] {0x23}); + NODE_PUBLIC(new int[] {0x1C}), // 28 in decimal + NODE_PRIVATE(new int[] {0x20}), // 32 in decimal + ACCOUNT_SECRET_KEY(new int[] {0x22}), // 34 in decimal + ACCOUNT_PUBLIC_KEY(new int[] {0x23}); // 35 in decimal private final int[] values; diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/Issue.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/Issue.java new file mode 100644 index 000000000..61c478b6a --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/Issue.java @@ -0,0 +1,52 @@ +package org.xrpl.xrpl4j.codec.binary.types; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.base.Preconditions; +import org.immutables.value.Value; + +import java.util.Optional; + +/** + * JSON mapping object for the Issue serializable type. + */ +@Value.Immutable +@JsonSerialize(as = ImmutableIssue.class) +@JsonDeserialize(as = ImmutableIssue.class) +public interface Issue { + + /** + * Construct a {@code Issue} builder. + * + * @return An {@link ImmutableIssue.Builder}. + */ + static ImmutableIssue.Builder builder() { + return ImmutableIssue.builder(); + } + + /** + * The currency code of the Issue. + * + * @return A {@link JsonNode} containing the currency code. + */ + JsonNode currency(); + + /** + * The address of the issuer of this currency. Will be empty if {@link #currency()} is XRP. + * + * @return An optionally present {@link JsonNode}. + */ + Optional issuer(); + + /** + * Validate that {@link #issuer()} is empty if {@link #currency()} is "XRP". + */ + @Value.Check + default void checkIssuerEmptyForXrp() { + if (currency().asText().equals("XRP")) { + Preconditions.checkState(!issuer().isPresent(), "If Issue is XRP, issuer must be empty."); + } + } + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/IssueType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/IssueType.java new file mode 100644 index 000000000..51ea134af --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/IssueType.java @@ -0,0 +1,62 @@ +package org.xrpl.xrpl4j.codec.binary.types; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; +import org.xrpl.xrpl4j.codec.binary.BinaryCodecObjectMapperFactory; +import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser; + +public class IssueType extends SerializedType { + + private static final ObjectMapper objectMapper = BinaryCodecObjectMapperFactory.getObjectMapper(); + + public IssueType() { + this(UnsignedByteArray.ofSize(20)); + } + + public IssueType(UnsignedByteArray bytes) { + super(bytes); + } + + @Override + public IssueType fromJson(JsonNode node) throws JsonProcessingException { + if (!node.isObject()) { + throw new IllegalArgumentException("node is not an object"); + } + + Issue issue = objectMapper.treeToValue(node, Issue.class); + + UnsignedByteArray byteArray = new CurrencyType().fromJson(issue.currency()).value(); + issue.issuer().ifPresent( + issuer -> byteArray.append(new AccountIdType().fromJson(issuer).value()) + ); + + return new IssueType(byteArray); + } + + @Override + public IssueType fromParser(BinaryParser parser) { + CurrencyType currency = new CurrencyType().fromParser(parser); + if (currency.toJson().asText().equals("XRP")) { + return new IssueType(currency.value()); + } + AccountIdType issuer = new AccountIdType().fromParser(parser); + return new IssueType(currency.value().append(issuer.value())); + } + + @Override + public JsonNode toJson() { + BinaryParser parser = new BinaryParser(this.toHex()); + JsonNode currency = new CurrencyType().fromParser(parser).toJson(); + + ImmutableIssue.Builder builder = Issue.builder(); + builder.currency(currency); + if (!currency.asText().equals("XRP")) { + JsonNode issuer = new AccountIdType().fromParser(parser).toJson(); + builder.issuer(issuer); + } + + return objectMapper.valueToTree(builder.build()); + } +} 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 47e10efce..cc67a3ff9 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 @@ -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. @@ -57,6 +57,7 @@ public abstract class SerializedType> { .put("UInt32", () -> new UInt32Type()) .put("UInt64", () -> new UInt64Type()) .put("Vector256", () -> new Vector256Type()) + .put("Issue", () -> new IssueType()) .build(); private final UnsignedByteArray bytes; diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/PrivateKey.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/PrivateKey.java index f8a5e4285..1880aaa0c 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/PrivateKey.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/PrivateKey.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. @@ -20,6 +20,7 @@ * =========================LICENSE_END================================== */ +import com.google.common.base.Preconditions; import org.xrpl.xrpl4j.codec.addresses.KeyType; import org.xrpl.xrpl4j.codec.addresses.UnsignedByte; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; @@ -32,23 +33,98 @@ public class PrivateKey implements PrivateKeyable, javax.security.auth.Destroyable { /** - * Keys generated from the secp256k1 curve have 33 bytes in XRP Ledger. However, keys derived from the ed25519 curve - * have only 32 bytes, and so get prefixed with this HEX value so that all keys in the ledger are 33 bytes. + * A one-byte prefix for ed25519 keys. + * + * @deprecated This value will be removed in a future version. Prefer {@link #ED2559_PREFIX} or + * {@link #SECP256K1_PREFIX} instead. */ + @Deprecated public static final UnsignedByte PREFIX = UnsignedByte.of(0xED); + /** + * Private keys (whether from the ed25519 or secp256k1 curves) have 32 bytes naturally. At the same time, secp256k1 + * public keys have 33 bytes naturally, whereas ed25519 public keys have 32 bytes naturally. Because of this, in XRPL, + * ed25519 public keys are prefixed with a one-byte prefix (i.e., 0xED). For consistency, this library (and other XRPL + * tooling) also prepends all private keys with artificial prefixes (0xED for ed25519 or 0x00 for secp256k1). This + * value is the one-byte prefix for ed25519 keys. + */ + public static final UnsignedByte ED2559_PREFIX = UnsignedByte.of(0xED); + + /** + * Private keys (whether from the ed25519 or secp256k1 curves) have 32 bytes naturally. At the same time, secp256k1 + * public keys have 33 bytes naturally, whereas ed25519 public keys have 32 bytes naturally. Because of this, in XRPL, + * ed25519 public keys are prefixed with a one-byte prefix (i.e., 0xED). For consistency, this library (and other XRPL + * tooling) also prepends all private keys with artificial prefixes (0xED for ed25519 or 0x00 for secp256k1). This + * value is the one-byte prefix for secp256k1 keys. + */ + public static final UnsignedByte SECP256K1_PREFIX = UnsignedByte.of(0x00); + private final UnsignedByteArray value; + + private final KeyType keyType; + private boolean destroyed; /** - * Instantiates a new instance of {@link PrivateKey} using the supplied bytes. + * Instantiates a new instance of {@link PrivateKey} using the supplied 32 bytes and specified key type. + * + * @param value An {@link UnsignedByteArray} containing a private key's natural bytes (i.e., 32 bytes). + * @param keyType A {@link KeyType} for this private key. + * + * @return A {@link PrivateKey}. + */ + public static PrivateKey fromNaturalBytes(final UnsignedByteArray value, final KeyType keyType) { + return new PrivateKey(value, keyType); // <-- rely on constructor for all validation and error messaging. + } + + /** + * Instantiates a new instance of {@link PrivateKey} using the supplied bytes by inspecting the first byte out of 33 + * to see which {@link KeyType} to assign. + * + * @param value An {@link UnsignedByteArray} containing a private key's natural bytes (i.e., 32 bytes). + * + * @return A {@link PrivateKey}. + */ + public static PrivateKey fromPrefixedBytes(final UnsignedByteArray value) { + Objects.requireNonNull(value); + + Preconditions.checkArgument(value.length() == 33, String.format( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but %s were supplied.", + value.length() + )); + + final UnsignedByte prefixByte = value.get(0); // <-- relies upon the above length check. + if (ED2559_PREFIX.equals(prefixByte)) { + return new PrivateKey(value.slice(1, 33), KeyType.ED25519); + } else if (SECP256K1_PREFIX.equals(prefixByte)) { + return new PrivateKey(value.slice(1, 33), KeyType.SECP256K1); + } else { + throw new IllegalArgumentException(String.format( + "PrivateKey construction requires 32 natural bytes plug a one-byte prefix value of either `0xED` for ed25519 " + + "private keys or `0x00` for secp256k1 private keys. Input byte length was %s bytes with a prefixByte value " + + "of `0x%s`", value.length(), prefixByte.hexValue()) + ); + } + } + + /** + * Instantiates a new instance of {@link PrivateKey} using the supplied bytes by inspecting the first byte out of 33 + * to see which {@link KeyType} to assign. * * @param value An {@link UnsignedByteArray} containing this key's binary value. * * @return A {@link PrivateKey}. + * + * @deprecated This method will be removed in a future version. Prefer {@link #fromPrefixedBytes(UnsignedByteArray)} + * instead. */ + @Deprecated public static PrivateKey of(final UnsignedByteArray value) { - return new PrivateKey(value); + // Assumption: Any developer using this method before it was deprecated (i.e., v3.2.1 and before) will likely have + // expected `value` to have been prefixed. However, until this method was "fixed" in v3.2.2, a developer might have + // used this method incorrectly because (1) this method had no length check and (2) the computation of the KeyType + // simply inspected the first byte, and naively defaulted to secp256k1 if the first byte was not `0xED`. + return fromPrefixedBytes(value); } /** @@ -56,17 +132,91 @@ public static PrivateKey of(final UnsignedByteArray value) { * * @param value An {@link UnsignedByteArray} for this key's value. */ - private PrivateKey(final UnsignedByteArray value) { - this.value = Objects.requireNonNull(value); + private PrivateKey(final UnsignedByteArray value, final KeyType keyType) { + Objects.requireNonNull(value); // <-- Check not-null first. + this.keyType = Objects.requireNonNull(keyType); + + // We assert this precondition here because this is a private constructor that can be fully tested via unit test, + // so this precondition should never be violated, and if it is, then it's a bug in xrpl4j. + Preconditions.checkArgument( + value.length() == 32, + "Byte values passed to this constructor must be 32 bytes long, with no prefix." + ); + + // NOTE: We do not do any further sanity checking of `value` (e.g., converting to a BigInteger and checking if it's + // in the proper range of [1, N-1]) on grounds that any particular underlying implementation will enforce these + // invariants for us, and will likely be more correct than we would. + + // Note: `UnsignedByteArray#toByteArray` will perform a copy, which is what we want in order to enforce + // immutability of this PrivateKey (because Java 8 doesn't support immutable byte arrays). + this.value = UnsignedByteArray.of(value.toByteArray()); // <- Always copy to ensure immutability } /** * Accessor for the key value, in binary (Note: will be 33 bytes). * * @return An instance of {@link UnsignedByteArray}. + * + * @deprecated Prefer {@link #prefixedBytes()} or {@link #naturalBytes()} instead. */ + @Deprecated public UnsignedByteArray value() { - return UnsignedByteArray.of(value.toByteArray()); + // Check for empty value, which can occur if this PrivateKey is "destroyed" but still in memory. + if (value.length() == 0) { + return UnsignedByteArray.empty(); + } else { + // This is technically wrong (because `value()` had an ambiguous meaning prior to fixing #486), but this mirrors + // what's in v3 prior to fixing #486, and will be fixed in v4 once the deprecated `.value()` is removed. + return this.prefixedBytes(); + } + } + + /** + * Accessor for the byte value in {@link #value()} but in a more natural form (i.e., the size of the returned value + * will be 32 bytes). Natural ed25519 or secp256k1 private keys will ordinarily contain only 32 bytes. However, in + * XRPL, private keys are represented with a single-byte prefix (i.e., `0xED` for ed25519 and `0x00` for secp256k1 + * keys). + * + * @return An instance of {@link UnsignedByteArray}. + */ + public UnsignedByteArray naturalBytes() { + // Check for empty value, which can occur if this PrivateKey is "destroyed" but still in memory. + if (value.length() == 0) { + return UnsignedByteArray.empty(); + } else { + // Note: `toByteArray()` will perform a copy, which is what we want in order to enforce immutability of this + // PrivateKey (because Java 8 doesn't support immutable byte arrays). + return UnsignedByteArray.of(value.toByteArray()); + } + } + + /** + * Accessor for the bytes of this private key in a prefixed. Natural ed25519 or secp256k1 private keys will ordinarily + * contain only 32 bytes. However, in XRPL, private keys are typically represented with a single-byte prefix (i.e., + * `0xED` for ed25519 and `0x00` for secp256k1 keys), which this method provides. + * + * @return An instance of {@link UnsignedByteArray}. + */ + public UnsignedByteArray prefixedBytes() { + // Check for empty value, which can occur if this PrivateKey is "destroyed" but still in memory. + if (value.length() == 0) { + return UnsignedByteArray.empty(); + } else { + switch (keyType) { + case ED25519: { + return UnsignedByteArray.of(ED2559_PREFIX) + .append(this.naturalBytes()); // <-- Forward to this function to guarantee copied bytes + } + case SECP256K1: { + return UnsignedByteArray.of(SECP256K1_PREFIX) + .append(this.naturalBytes()); // <-- Forward to this function to guarantee copied bytes + } + default: { + // This should never happen; if it does, there's a bug in this implementation + throw new IllegalStateException(String.format("Invalid keyType=%s", keyType)); + } + } + } } /** @@ -75,8 +225,7 @@ public UnsignedByteArray value() { * @return A {@link KeyType}. */ public final KeyType keyType() { - final UnsignedByte prefixByte = this.value().get(0); - return prefixByte.equals(PREFIX) ? KeyType.ED25519 : KeyType.SECP256K1; + return this.keyType; } @Override @@ -95,25 +244,25 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - if (!(obj instanceof PrivateKey)) { + if (obj == null || getClass() != obj.getClass()) { return false; } - PrivateKey that = (PrivateKey) obj; - - return value.equals(that.value); + return Objects.equals(value, that.value) && keyType == that.keyType; } @Override public int hashCode() { - return value.hashCode(); + return Objects.hash(value, keyType); } @Override public String toString() { - return "PrivateKey{" + - "value=[redacted]" + - ", destroyed=" + destroyed + - '}'; + return String.format("PrivateKey{" + + "value=[redacted]," + + "keyType=%s," + + "destroyed=%s" + + "}", this.keyType(), this.isDestroyed() + ); } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/PublicKey.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/PublicKey.java index 5a07e795c..84e99416d 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/PublicKey.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/PublicKey.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. @@ -35,6 +35,7 @@ import org.xrpl.xrpl4j.codec.addresses.Decoded; import org.xrpl.xrpl4j.codec.addresses.KeyType; import org.xrpl.xrpl4j.codec.addresses.PublicKeyCodec; +import org.xrpl.xrpl4j.codec.addresses.UnsignedByte; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.codec.addresses.Version; import org.xrpl.xrpl4j.model.jackson.modules.PublicKeyDeserializer; @@ -53,12 +54,18 @@ public interface PublicKey { /** - * Multi-signed transactions must contain an empty String in the SigningPublicKey field. This constant - * is an {@link PublicKey} that can be used as the {@link Transaction#signingPublicKey()} value for multi-signed + * A one-byte prefix for ed25519 keys. In XRPL, ed25519 public keys are prefixed with a one-byte prefix (i.e., `0xED`) + * in order to be consistent with secp256k1 public keys, which always have 33 bytes. + */ + UnsignedByte ED2559_PREFIX = UnsignedByte.of(0xED); + + /** + * Multi-signed transactions must contain an empty String in the SigningPublicKey field. This constant is an + * {@link PublicKey} that can be used as the {@link Transaction#signingPublicKey()} value for multi-signed * transactions. */ PublicKey MULTI_SIGN_PUBLIC_KEY = PublicKey.builder().value(UnsignedByteArray.empty()).build(); - + /** * Instantiates a new builder. * @@ -77,11 +84,11 @@ static ImmutablePublicKey.Builder builder() { */ static PublicKey fromBase58EncodedPublicKey(final String base58EncodedPublicKey) { Objects.requireNonNull(base58EncodedPublicKey); - + if (base58EncodedPublicKey.isEmpty()) { return MULTI_SIGN_PUBLIC_KEY; } - + return PublicKey.builder() .value(PublicKeyCodec.getInstance().decodeAccountPublicKey(base58EncodedPublicKey)) .build(); @@ -100,7 +107,7 @@ static PublicKey fromBase16EncodedPublicKey(final String base16EncodedPublicKey) if (base16EncodedPublicKey.isEmpty()) { return MULTI_SIGN_PUBLIC_KEY; } - + return PublicKey.builder() .value(UnsignedByteArray.of(BaseEncoding.base16().decode(base16EncodedPublicKey.toUpperCase()))) .build(); @@ -123,7 +130,7 @@ default String base58Value() { if (value().length() == 0) { return ""; } - + return PublicKeyCodec.getInstance().encodeAccountPublicKey(this.value()); } @@ -175,5 +182,5 @@ static UnsignedByteArray computePublicKeyHash(final UnsignedByteArray publicKey) digest.doFinal(ripemdSha256, 0); return UnsignedByteArray.of(ripemdSha256); } - + } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/Seed.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/Seed.java index 4e62dfd29..c2eabd2e9 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/Seed.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/Seed.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. @@ -21,6 +21,7 @@ */ import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.hash.Hashing; import com.google.common.primitives.UnsignedInteger; @@ -34,11 +35,11 @@ import org.xrpl.xrpl4j.codec.addresses.Decoded; import org.xrpl.xrpl4j.codec.addresses.KeyType; import org.xrpl.xrpl4j.codec.addresses.SeedCodec; -import org.xrpl.xrpl4j.codec.addresses.UnsignedByte; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.codec.addresses.Version; import org.xrpl.xrpl4j.codec.addresses.exceptions.DecodeException; import org.xrpl.xrpl4j.crypto.HashingUtils; +import org.xrpl.xrpl4j.crypto.signing.bc.Secp256k1; import java.math.BigInteger; import java.util.Objects; @@ -172,6 +173,7 @@ static Seed secp256k1SeedFromEntropy(final Entropy entropy) { * @param base58EncodedSecret A base58-encoded {@link String} that represents an encoded seed. * * @return A {@link Seed}. + * * @see "https://xrpl.org/xrp-testnet-faucet.html" */ static Seed fromBase58EncodedSecret(final Base58EncodedSecret base58EncodedSecret) { @@ -309,20 +311,18 @@ public static KeyPair deriveKeyPair(final Seed seed) { } UnsignedByteArray rawPrivateKey = HashingUtils.sha512Half(decoded.bytes()); + // `rawPrivateKey` will be 32 bytes due to hashing, so no padding required. Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(rawPrivateKey.toByteArray(), 0); Ed25519PublicKeyParameters publicKey = privateKey.generatePublicKey(); - // XRPL ED25519 keys are prefixed with 0xED so that the keys are 33 bytes and match the length of secp256k1 - // keys. Note that Bouncy Castle only deals with 32 byte keys, so we need to manually add the prefix - final UnsignedByte prefix = UnsignedByte.of(0xED); - final UnsignedByteArray prefixedPrivateKey = UnsignedByteArray.of(prefix) - .append(UnsignedByteArray.of(privateKey.getEncoded())); - final UnsignedByteArray prefixedPublicKey = UnsignedByteArray.of(prefix) + // ed25519 public keys in XRPL have a one-byte prefix of `0xED` so that all public keys have 33 bytes (this is + // to conform with secp256k1 public keys, which are 33 bytes long and have a `0x00` byte prefix. + final UnsignedByteArray prefixedPublicKey = UnsignedByteArray.of(PublicKey.ED2559_PREFIX) .append(UnsignedByteArray.of(publicKey.getEncoded())); return KeyPair.builder() - .privateKey(PrivateKey.of(prefixedPrivateKey)) + .privateKey(PrivateKey.fromNaturalBytes(UnsignedByteArray.of(privateKey.getEncoded()), KeyType.ED25519)) .publicKey(PublicKey.fromBase16EncodedPublicKey(prefixedPublicKey.hexValue())) .build(); } @@ -382,15 +382,17 @@ public static KeyPair deriveKeyPair(final Seed seed) { private static KeyPair deriveKeyPair(final UnsignedByteArray seedBytes, final int accountNumber) { Objects.requireNonNull(seedBytes); - // private key needs to be a BigInteger so we can derive the public key by multiplying G by the private key. + // private key needs to be a BigInteger, so we can derive the public key by multiplying G by the private key. final BigInteger privateKeyInt = derivePrivateKey(seedBytes, accountNumber); - final UnsignedByteArray publicKeyInt = derivePublicKey(privateKeyInt); + + // This derivePublicKey will pad to 33 bytes. + final UnsignedByteArray publicKeyByteArray = derivePublicKey(privateKeyInt); + // This merely enforces the invariant that should be defined in `derivePublicKey(privateKeyInt);` + Preconditions.checkArgument(publicKeyByteArray.length() == 33, "Length was " + publicKeyByteArray.length()); return KeyPair.builder() - .privateKey(PrivateKey.of(UnsignedByteArray.of(privateKeyInt.toByteArray()))) - .publicKey(PublicKey.fromBase16EncodedPublicKey( - UnsignedByteArray.of(publicKeyInt.toByteArray()).hexValue() - )) + .privateKey(PrivateKey.fromPrefixedBytes(Secp256k1.toUnsignedByteArray(privateKeyInt, 33))) + .publicKey(PublicKey.builder().value(publicKeyByteArray).build()) .build(); } @@ -403,13 +405,17 @@ private static KeyPair deriveKeyPair(final UnsignedByteArray seedBytes, final in */ private static UnsignedByteArray derivePublicKey(final BigInteger privateKey) { Objects.requireNonNull(privateKey); - return UnsignedByteArray.of(EC_DOMAIN_PARAMETERS.getG().multiply(privateKey).getEncoded(true)); + + UnsignedByteArray unpaddedBytes = UnsignedByteArray.of( + EC_DOMAIN_PARAMETERS.getG().multiply(privateKey).getEncoded(true)); + + return Secp256k1.withZeroPrefixPadding(unpaddedBytes, 33); // <-- Ensure returned UBA has 33 bytes. } /** * Derive a public key from the supplied {@code seed} and {@code accountNumber}. * - * @param seed A {@link UnsignedByteArray} representing a seed that can be used to generated an XRPL + * @param seed A {@link UnsignedByteArray} representing a seed that can be used to generate an XRPL * address. * @param accountNumber An integer representing the account nunmber. * diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/bc/BcKeyUtils.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/bc/BcKeyUtils.java index 12d7d34e5..0b8df8cb3 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/bc/BcKeyUtils.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/keys/bc/BcKeyUtils.java @@ -37,6 +37,7 @@ import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.crypto.keys.PrivateKey; import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.crypto.signing.bc.Secp256k1; import java.math.BigInteger; import java.security.Security; @@ -84,10 +85,11 @@ public static PrivateKey toPrivateKey(final Ed25519PrivateKeyParameters ed25519P Objects.requireNonNull(ed25519PrivateKeyParameters); // XRPL ED25519 keys are prefixed with 0xED so that the keys are 33 bytes and match the length of sekp256k1 keys. - // Bouncy Castle only deals with 32 byte keys, so we need to manually add the prefix - UnsignedByteArray prefixedPrivateKey = UnsignedByteArray.of(PrivateKey.PREFIX) - .append(UnsignedByteArray.of(ed25519PrivateKeyParameters.getEncoded())); - return PrivateKey.of(prefixedPrivateKey); + // Bouncy Castle only deals with 32 byte keys, but in XRPL these often include a one byte prefix (i.e., `0xED`) to + // indicate this is an ed25519 private key. However, this is taken care of by the `PrivateKey.fromNaturalBytes`. + return PrivateKey.fromNaturalBytes( + UnsignedByteArray.of(ed25519PrivateKeyParameters.getEncoded()), KeyType.ED25519 + ); } /** @@ -98,10 +100,10 @@ public static PrivateKey toPrivateKey(final Ed25519PrivateKeyParameters ed25519P * @return A {@link PrivateKey}. */ public static PrivateKey toPrivateKey(final ECPrivateKeyParameters ecPrivateKeyParameters) { - // Convert the HEX representation of the BigInteger into bytes. - byte[] privateKeyBytes = BaseEncoding.base16().decode(ecPrivateKeyParameters.getD().toString(16).toUpperCase()); - final UnsignedByteArray privateKeyUba = UnsignedByteArray.of(privateKeyBytes); - return PrivateKey.of(privateKeyUba); + return PrivateKey.fromPrefixedBytes( + // Call `UnsignedByteArray.from` to properly prefix-pad the BigInteger's bytes. + Secp256k1.toUnsignedByteArray(ecPrivateKeyParameters.getD(), 33) + ); } /** @@ -134,7 +136,7 @@ public static PublicKey toPublicKey(final Ed25519PublicKeyParameters ed25519Publ Objects.requireNonNull(ed25519PublicKeyParameters); // XRPL ED25519 keys are prefixed with 0xED so that the keys are 33 bytes and match the length of sekp256k1 keys. // Bouncy Castle only deals with 32 byte keys, so we need to manually add the prefix - UnsignedByteArray prefixedPublicKey = UnsignedByteArray.of(PrivateKey.PREFIX) + UnsignedByteArray prefixedPublicKey = UnsignedByteArray.of(PublicKey.ED2559_PREFIX) .append(UnsignedByteArray.of(ed25519PublicKeyParameters.getEncoded())); return PublicKey.builder() @@ -203,7 +205,8 @@ public static PublicKey toPublicKey(final PrivateKey privateKey) { public static Ed25519PrivateKeyParameters toEd25519PrivateKeyParams(PrivateKey privateKey) { Objects.requireNonNull(privateKey); Preconditions.checkArgument(privateKey.keyType() == ED25519); - return new Ed25519PrivateKeyParameters(privateKey.value().toByteArray(), 1); // <-- Strip leading prefix byte. + // Use offset 0 with no prefix + return new Ed25519PrivateKeyParameters(privateKey.naturalBytes().toByteArray(), 0); } /** @@ -233,9 +236,9 @@ public static ECPrivateKeyParameters toEcPrivateKeyParams(final PrivateKey priva Objects.requireNonNull(privateKey); Preconditions.checkArgument(privateKey.keyType() == KeyType.SECP256K1, "KeyType must be SECP256K1"); - final BigInteger privateKeyInt = new BigInteger( - BaseEncoding.base16().encode(privateKey.value().toByteArray()), 16 - ); - return new ECPrivateKeyParameters(privateKeyInt, BcKeyUtils.PARAMS); + // From http://www.secg.org/sec1-v2.pdf: A PrivateKey consists of an elliptic curve secret key `d` which is an + // integer in the interval [1, n − 1]. Therefore, it is safe to assume that the signum below should always be 1. + final BigInteger secretKeyD = new BigInteger(1, privateKey.naturalBytes().toByteArray()); + return new ECPrivateKeyParameters(secretKeyD, BcKeyUtils.PARAMS); } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractTransactionSigner.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractTransactionSigner.java index 3a529d490..f30cdbe49 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractTransactionSigner.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractTransactionSigner.java @@ -31,7 +31,9 @@ import java.util.Objects; /** - * An abstract implementation of {@link SignatureService} with common functionality that sub-classes can utilize. + * An abstract implementation of {@link SignatureService} with common functionality that subclasses can utilize. + * + * @param

A type that extends {@link PrivateKeyable}. */ public abstract class AbstractTransactionSigner

implements TransactionSigner

{ diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java index 6ade9325c..7f8622555 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java @@ -31,9 +31,16 @@ import org.xrpl.xrpl4j.model.transactions.AccountDelete; import org.xrpl.xrpl4j.model.transactions.AccountSet; import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.AmmBid; +import org.xrpl.xrpl4j.model.transactions.AmmCreate; +import org.xrpl.xrpl4j.model.transactions.AmmDelete; +import org.xrpl.xrpl4j.model.transactions.AmmDeposit; +import org.xrpl.xrpl4j.model.transactions.AmmVote; +import org.xrpl.xrpl4j.model.transactions.AmmWithdraw; import org.xrpl.xrpl4j.model.transactions.CheckCancel; import org.xrpl.xrpl4j.model.transactions.CheckCash; import org.xrpl.xrpl4j.model.transactions.CheckCreate; +import org.xrpl.xrpl4j.model.transactions.Clawback; import org.xrpl.xrpl4j.model.transactions.DepositPreAuth; import org.xrpl.xrpl4j.model.transactions.EscrowCancel; import org.xrpl.xrpl4j.model.transactions.EscrowCreate; @@ -271,6 +278,34 @@ public SingleSignedTransaction addSignatureToTransact transactionWithSignature = TicketCreate.builder().from((TicketCreate) transaction) .transactionSignature(signature) .build(); + } else if (Clawback.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignature = Clawback.builder().from((Clawback) transaction) + .transactionSignature(signature) + .build(); + } else if (AmmBid.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignature = AmmBid.builder().from((AmmBid) transaction) + .transactionSignature(signature) + .build(); + } else if (AmmCreate.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignature = AmmCreate.builder().from((AmmCreate) transaction) + .transactionSignature(signature) + .build(); + } else if (AmmDeposit.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignature = AmmDeposit.builder().from((AmmDeposit) transaction) + .transactionSignature(signature) + .build(); + } else if (AmmVote.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignature = AmmVote.builder().from((AmmVote) transaction) + .transactionSignature(signature) + .build(); + } else if (AmmWithdraw.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignature = AmmWithdraw.builder().from((AmmWithdraw) transaction) + .transactionSignature(signature) + .build(); + } else if (AmmDelete.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignature = AmmDelete.builder().from((AmmDelete) transaction) + .transactionSignature(signature) + .build(); } else { // Should never happen, but will in a unit test if we miss one. throw new IllegalArgumentException("Signing fields could not be added to the transaction."); @@ -405,6 +440,34 @@ public T addMultiSignaturesToTransaction(T transaction, transactionWithSignatures = TicketCreate.builder().from((TicketCreate) transaction) .signers(signers) .build(); + } else if (Clawback.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignatures = Clawback.builder().from((Clawback) transaction) + .signers(signers) + .build(); + } else if (AmmBid.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignatures = AmmBid.builder().from((AmmBid) transaction) + .signers(signers) + .build(); + } else if (AmmCreate.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignatures = AmmCreate.builder().from((AmmCreate) transaction) + .signers(signers) + .build(); + } else if (AmmDeposit.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignatures = AmmDeposit.builder().from((AmmDeposit) transaction) + .signers(signers) + .build(); + } else if (AmmVote.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignatures = AmmVote.builder().from((AmmVote) transaction) + .signers(signers) + .build(); + } else if (AmmWithdraw.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignatures = AmmWithdraw.builder().from((AmmWithdraw) transaction) + .signers(signers) + .build(); + } else if (AmmDelete.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignatures = AmmDelete.builder().from((AmmDelete) transaction) + .signers(signers) + .build(); } else { // Should never happen, but will in a unit test if we miss one. throw new IllegalArgumentException("Signing fields could not be added to the transaction."); diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/BcSignatureService.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/BcSignatureService.java index 310df2616..977281264 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/BcSignatureService.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/BcSignatureService.java @@ -94,30 +94,21 @@ protected synchronized Signature edDsaSign( Objects.requireNonNull(privateKey); Objects.requireNonNull(signableTransactionBytes); - final byte[] privateKeyBytes = new byte[32]; - try { - // Remove ED prefix byte (if it's there) - System.arraycopy(privateKey.value().toByteArray(), 1, privateKeyBytes, 0, 32); - Ed25519PrivateKeyParameters privateKeyParameters = new Ed25519PrivateKeyParameters( - privateKeyBytes, 0 - ); - - ed25519Signer.reset(); - ed25519Signer.init(true, privateKeyParameters); - ed25519Signer.update( - signableTransactionBytes.toByteArray(), 0, signableTransactionBytes.getUnsignedBytes().size() - ); - - final UnsignedByteArray sigBytes = UnsignedByteArray.of(ed25519Signer.generateSignature()); - return Signature.builder() - .value(sigBytes) - .build(); - } finally { - // Clear out the copied array, which was only used for signing. - for (int i = 0; i < 32; i++) { - privateKeyBytes[i] = (byte) 0; - } - } + final Ed25519PrivateKeyParameters privateKeyParameters = BcKeyUtils.toEd25519PrivateKeyParams(privateKey); + + final byte[] signableBytes = signableTransactionBytes.toByteArray(); + + ed25519Signer.reset(); + ed25519Signer.init(true, privateKeyParameters); + ed25519Signer.update(signableBytes, 0, signableBytes.length); + + final UnsignedByteArray sigBytes = UnsignedByteArray.of(ed25519Signer.generateSignature()); + return Signature.builder() + .value(sigBytes) + .build(); + + // Note: Ed25519PrivateKeyParameters does not provide a destroy function, but it will be eligible for cleanup (in + // the next GC) once this function exits. } @SuppressWarnings("checkstyle:LocalVariableName") @@ -128,10 +119,9 @@ protected synchronized Signature ecDsaSign(final PrivateKey privateKey, final Un final UnsignedByteArray messageHash = HashingUtils.sha512Half(transactionBytes); - final BigInteger privateKeyInt = new BigInteger(privateKey.value().toByteArray()); - final ECPrivateKeyParameters parameters = new ECPrivateKeyParameters(privateKeyInt, BcKeyUtils.PARAMS); + final ECPrivateKeyParameters ecPrivateKeyParams = BcKeyUtils.toEcPrivateKeyParams(privateKey); - ecdsaSigner.init(true, parameters); + ecdsaSigner.init(true, ecPrivateKeyParams); final BigInteger[] signatures = ecdsaSigner.generateSignature(messageHash.toByteArray()); final BigInteger r = signatures[0]; BigInteger s = signatures[1]; diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/Secp256k1.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/Secp256k1.java index 6097303a5..c747b37c3 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/Secp256k1.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/Secp256k1.java @@ -20,17 +20,20 @@ * =========================LICENSE_END================================== */ +import com.google.common.base.Preconditions; import org.bouncycastle.asn1.sec.SECNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.params.ECDomainParameters; +import org.xrpl.xrpl4j.codec.addresses.UnsignedByte; +import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.crypto.keys.bc.BcKeyUtils; +import java.math.BigInteger; +import java.util.Objects; + /** * Static constants for Secp256k1 operations. - * - * @deprecated This interface is deprecated in-favor of {@link BcKeyUtils#PARAMS}. */ -@Deprecated public interface Secp256k1 { /** @@ -54,4 +57,95 @@ public interface Secp256k1 { EC_PARAMETERS.getH() ); + /** + * Private keys (whether from the ed25519 or secp256k1 curves) have 32 bytes naturally. At the same time, secp256k1 + * public keys have 33 bytes naturally, whereas ed25519 public keys have 32 bytes naturally. Because of this, in XRPL, + * ed25519 public keys are prefixed with a one-byte prefix (i.e., 0xED). For consistency, this library (and other XRPL + * tooling) also prepends all private keys with artificial prefixes (0xED for ed25519 or 0x00 for secp256k1). This + * value is the one-byte prefix for secp256k1 keys. + */ + UnsignedByte SECP256K1_PREFIX = UnsignedByte.of(0x00); + + /** + * Creates an {@link UnsignedByteArray} from the bytes of a supplied {@link BigInteger}. If the length of the + * resulting array is not at least {@code minFinalByteLength}, then the result is prefix padded with `0x00` bytes + * until the final array length is {@code minFinalByteLength}. + * + *

This function exists to ensure that transformation of secp256k1 private keys from a {@link BigInteger} to a + * byte array are done in a consistent manner, always yielding the desired number of bytes. For example, secp256k1 + * private keys are 32-bytes long naturally. However, when transformed to a byte array via + * {@link BigInteger#toByteArray()}, the result will not always have the same number of leading zero bytes that one + * might expect. Sometimes the returned array will have 33 bytes, one of which is a zero-byte prefix pad that is meant + * to ensure the underlying number is not represented as a negative number. Other times, the array will have fewer + * than 32 bytes, for example 31 or even 30, if the byte array has redundant leading zero bytes. + * + *

Note that this function assumes the supplied {@code amount} is always positive, which roughly correlates with + * the secp256k1 requirement that private key scalar `D` values be in the range [1, N-1]. + * + *

Thus, this function can be used to normalize a byte array containing a secp256k1 private key with a desired + * number of 0-byte padding to ensure that it is always the desired {@code minFinalByteLength} (e.g., in this library, + * secp256k1 private keys should always be comprised of a 32-byte natural private key with a one-byte `0x00` prefix + * pad). + * + * @param amount A {@link BigInteger} to convert into an {@link UnsignedByteArray}. + * @param minFinalByteLength The minimum length, in bytes, that the final result must be. If the final byte length is + * less than this number, the resulting array will be prefix padded to increase its length + * to this number. + * + * @return An {@link UnsignedByteArray} with a length of at least {@code minFinalByteLength}. + * + * @see "https://github.com/XRPLF/xrpl4j/issues/486" + */ + static UnsignedByteArray toUnsignedByteArray(final BigInteger amount, int minFinalByteLength) { + Objects.requireNonNull(amount); + Preconditions.checkArgument(amount.signum() >= 0, "amount must not be negative"); + Preconditions.checkArgument(minFinalByteLength >= 0, "minFinalByteLength must not be negative"); + + // Return the `amount` as an UnsignedByteArray, but with the proper zero-byte prefix padding. + return withZeroPrefixPadding(amount.toByteArray(), minFinalByteLength); + } + + /** + * Construct a new {@link UnsignedByteArray} that contains the bytes from {@code bytes}, but with enough `0x00` prefix + * padding bytes such that the final length of the returned value is {@code minFinalByteLength}. + * + * @param bytes An {@link UnsignedByteArray} to zero-pad. + * @param minFinalByteLength The minimum length, in bytes, that the final result must be zero-byte prefix-padded to. + * If this number is greater-than {@code #length}, then this value will be reduced to + * {@code #length}. + * + * @return A copy of this {@link UnsignedByteArray} that has been zero-byte prefix-padded such that its final length + * is at least {@code minFinalByteLength}. + */ + static UnsignedByteArray withZeroPrefixPadding(final UnsignedByteArray bytes, int minFinalByteLength) { + Preconditions.checkArgument(minFinalByteLength >= 0, "minFinalByteLength must not be negative"); + + return withZeroPrefixPadding(bytes.toByteArray(), minFinalByteLength); + } + + /** + * Construct a new {@link UnsignedByteArray} that contains the bytes from {@code bytes}, but with enough `0x00` prefix + * padding bytes such that the final length of the returned value is {@code minFinalByteLength}. + * + * @param bytes A byte array to zero-pad. + * @param minFinalByteLength The minimum length, in bytes, that the final result must be zero-byte prefix-padded to. + * If this number is greater-than {@code #length}, then this value will be reduced to + * {@code #length}. + * + * @return A copy of this {@link UnsignedByteArray} that has been zero-byte prefix-padded such that its final length + * is at least {@code minFinalByteLength}. + */ + static UnsignedByteArray withZeroPrefixPadding(final byte[] bytes, int minFinalByteLength) { + Preconditions.checkArgument(minFinalByteLength >= 0, "minFinalByteLength must not be negative"); + + if (bytes.length > minFinalByteLength) { // <-- Increase `minFinalByteLength` to be at least bytes.length + minFinalByteLength = bytes.length; + } + + final int numPadBytes = minFinalByteLength - bytes.length; // <-- numPadBytes will never be negative + byte[] resultBytes = new byte[minFinalByteLength]; + System.arraycopy(bytes, 0, resultBytes, numPadBytes, bytes.length); + return UnsignedByteArray.of(resultBytes); + } + } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/XrplMethods.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/XrplMethods.java index 2c038ad4a..13e765602 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/XrplMethods.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/XrplMethods.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. @@ -20,6 +20,8 @@ * =========================LICENSE_END================================== */ +import com.google.common.annotations.Beta; + /** * A definition class for all rippled method name constants. */ @@ -176,6 +178,12 @@ public class XrplMethods { */ public static final String RIPPLE_PATH_FIND = "ripple_path_find"; + /** + * Constant for the ripple_path_find rippled API method. + */ + @Beta + public static final String AMM_INFO = "amm_info"; + // Payment Channel methods /** * Constant for the channel_authorize rippled API method. diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfo.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfo.java new file mode 100644 index 000000000..94a81b4cd --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfo.java @@ -0,0 +1,121 @@ +package org.xrpl.xrpl4j.model.client.amm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.client.XrplResult; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.CurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TradingFee; + +import java.util.List; +import java.util.Optional; + +/** + * Information about the requested AMM ledger entry. This response is very closely related to + * {@link org.xrpl.xrpl4j.model.ledger.AmmObject}, however rippled returns the object in a different format in + * responses to {@code amm_info} RPC requests. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmInfo.class) +@JsonDeserialize(as = ImmutableAmmInfo.class) +@Beta +public interface AmmInfo extends XrplResult { + + /** + * Construct a {@code AmmInfo} builder. + * + * @return An {@link ImmutableAmmInfo.Builder}. + */ + static ImmutableAmmInfo.Builder builder() { + return ImmutableAmmInfo.builder(); + } + + /** + * The address of the special account that holds this AMM's assets. + * + * @return An {@link Address}. + */ + @JsonProperty("account") + Address account(); + + /** + * The definition for one of the two assets this AMM holds. + * + * @return A {@link CurrencyAmount}. + */ + @JsonProperty("amount") + CurrencyAmount amount(); + + /** + * The definition for the other asset this AMM holds. + * + * @return A {@link CurrencyAmount}. + */ + @JsonProperty("amount2") + CurrencyAmount amount2(); + + /** + * Whether the first asset of the AMM is frozen. Always false is the first asset is XRP. + * + * @return {@code true} if asset 1 is frozen, otherwise {@code false}. + */ + @Value.Default + @JsonProperty("asset_frozen") + default boolean assetFrozen() { + return false; + } + + /** + * Whether the second asset of the AMM is frozen. Always false is the second asset is XRP. + * + * @return {@code true} if asset 2 is frozen, otherwise {@code false}. + */ + @Value.Default + @JsonProperty("asset2_frozen") + default boolean asset2Frozen() { + return false; + } + + /** + * Details of the current owner of the auction slot. + * + * @return An {@link AmmInfoAuctionSlot}. + */ + @JsonProperty("auction_slot") + Optional auctionSlot(); + + /** + * The total outstanding balance of liquidity provider tokens from this AMM instance. The holders of these tokens + * can vote on the AMM's trading fee in proportion to their holdings, or redeem the tokens for a share of the AMM's + * assets which grows with the trading fees collected. + * + * @return An {@link IssuedCurrencyAmount}. + */ + @JsonProperty("lp_token") + IssuedCurrencyAmount lpToken(); + + /** + * The percentage fee to be charged for trades against this AMM instance, in units of 1/10,000. The maximum value is + * 1000, for a 1% fee. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("trading_fee") + TradingFee tradingFee(); + + /** + * A list of vote objects, representing votes on the pool's trading fee. + * + * @return A {@link List} of {@link AmmInfoVoteEntry}s. + */ + @JsonProperty("vote_slots") + List voteSlots(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoAuctionSlot.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoAuctionSlot.java new file mode 100644 index 000000000..5583a68e1 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoAuctionSlot.java @@ -0,0 +1,91 @@ +package org.xrpl.xrpl4j.model.client.amm; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import com.google.common.primitives.UnsignedInteger; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TradingFee; + +import java.time.ZonedDateTime; +import java.util.List; + +/** + * Object mapping for an AMM auction slot returned in response to an {@code amm_info} RPC call. The structure + * of the response object is similar but has a slightly different format from + * {@link org.xrpl.xrpl4j.model.ledger.AuctionSlot}. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmInfoAuctionSlot.class) +@JsonDeserialize(as = ImmutableAmmInfoAuctionSlot.class) +@Beta +public interface AmmInfoAuctionSlot { + + /** + * Construct a {@code AmmInfoAuctionSlot} builder. + * + * @return An {@link ImmutableAmmInfoAuctionSlot.Builder}. + */ + static ImmutableAmmInfoAuctionSlot.Builder builder() { + return ImmutableAmmInfoAuctionSlot.builder(); + } + + /** + * The current owner of this auction slot. + * + * @return An {@link Address}. + */ + @JsonProperty("account") + Address account(); + + /** + * A list of at most 4 additional accounts that are authorized to trade at the discounted fee for this AMM instance. + * + * @return A {@link List} of {@link AmmInfoAuthAccount}s. + */ + @JsonProperty("auth_accounts") + List authAccounts(); + + /** + * The trading fee to be charged to the auction owner. By default this is 0, meaning that the auction owner can trade + * at no fee instead of the standard fee for this AMM. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("discounted_fee") + TradingFee discountedFee(); + + /** + * The time when this slot expires, as a {@link ZonedDateTime}. + * + * @return An {@link ZonedDateTime} + */ + @JsonProperty("expiration") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ", locale = "en_US") + ZonedDateTime expiration(); + + /** + * The amount the auction owner paid to win this slot, in LP Tokens. + * + * @return An {@link IssuedCurrencyAmount}. + */ + @JsonProperty("price") + IssuedCurrencyAmount price(); + + /** + * An {@link UnsignedInteger} between 1 and 20 denoting the time slot used for the continuous auction slot pricing + * mechanism of the AMM. + * + * @return An {@link UnsignedInteger}. + */ + @JsonProperty("time_interval") + UnsignedInteger timeInterval(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoAuthAccount.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoAuthAccount.java new file mode 100644 index 000000000..6df3907dc --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoAuthAccount.java @@ -0,0 +1,50 @@ +package org.xrpl.xrpl4j.model.client.amm; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.transactions.Address; + +/** + * An account that is authorized to trade at the discounted fee for an AMM instance. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmInfoAuthAccount.class) +@JsonDeserialize(as = ImmutableAmmInfoAuthAccount.class) +@Beta +public interface AmmInfoAuthAccount { + + /** + * Construct a {@code AmmInfoAuthAccount} builder. + * + * @return An {@link ImmutableAmmInfoAuthAccount.Builder}. + */ + static ImmutableAmmInfoAuthAccount.Builder builder() { + return ImmutableAmmInfoAuthAccount.builder(); + } + + /** + * Construct an {@link AmmInfoAuthAccount} containing the given {@link Address}. + * + * @param account An {@link Address}. + * + * @return An {@link AmmInfoAuthAccount} containing the address. + */ + static AmmInfoAuthAccount of(Address account) { + return builder() + .account(account) + .build(); + } + + /** + * The address of the authorized account. + * + * @return An {@link Address}. + */ + Address account(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoRequestParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoRequestParams.java new file mode 100644 index 000000000..622af735e --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoRequestParams.java @@ -0,0 +1,82 @@ +package org.xrpl.xrpl4j.model.client.amm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.client.XrplRequestParams; +import org.xrpl.xrpl4j.model.ledger.Issue; +import org.xrpl.xrpl4j.model.transactions.Address; + +import java.util.Optional; + +/** + * Request parameters for the {@code amm_info} rippled API method. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmInfoRequestParams.class) +@JsonDeserialize(as = ImmutableAmmInfoRequestParams.class) +@Beta +public interface AmmInfoRequestParams extends XrplRequestParams { + + /** + * Construct a new {@link AmmInfoRequestParams} that specifies the AMM account to query. + * + * @param ammAccount The {@link Address} of the AMM account. + * + * @return An {@link AmmInfoRequestParams}. + */ + static AmmInfoRequestParams from(Address ammAccount) { + return ImmutableAmmInfoRequestParams.builder() + .ammAccount(ammAccount) + .build(); + } + + /** + * Construct a new {@link AmmInfoRequestParams} that specifies {@code asset} and {@code asset2}. + * + * @param asset The first asset of the AMM, as an {@link Issue}. + * @param asset2 The second asset of the AMM, as an {@link Issue}. + * + * @return An {@link AmmInfoRequestParams}. + */ + static AmmInfoRequestParams from(Issue asset, Issue asset2) { + return ImmutableAmmInfoRequestParams.builder() + .asset(asset) + .asset2(asset2) + .build(); + } + + /** + * The address of the AMM's special AccountRoot. (This is the issuer of the AMM's LP Tokens). + * + *

If this field is specified, {@link #asset()} and {@link #asset2()} must be empty.

+ * + * @return An {@link Optional} {@link Address}. + */ + @JsonProperty("amm_account") + Optional
ammAccount(); + + /** + * One of the assets of the AMM to look up. + * + *

If this field is specified, {@link #asset2()} must be present, and {@link #ammAccount()} must be empty.

+ * + * @return An {@link Issue}. + */ + Optional asset(); + + /** + * The other of the assets of the AMM. + * + *

If this field is specified, {@link #asset()} must be present, and {@link #ammAccount()} must be empty.

+ * + * @return An {@link Issue}. + */ + Optional asset2(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoResult.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoResult.java new file mode 100644 index 000000000..b7edbfd40 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoResult.java @@ -0,0 +1,119 @@ +package org.xrpl.xrpl4j.model.client.amm; + +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.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.client.XrplResult; +import org.xrpl.xrpl4j.model.client.common.LedgerIndex; +import org.xrpl.xrpl4j.model.transactions.Hash256; + +import java.util.Optional; + +/** + * The result of an "amm_info" rippled API method call. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmInfoResult.class) +@JsonDeserialize(as = ImmutableAmmInfoResult.class) +@Beta +public interface AmmInfoResult extends XrplResult { + + /** + * Construct a {@code AmmInfoResult} builder. + * + * @return An {@link ImmutableAmmInfoResult.Builder}. + */ + static ImmutableAmmInfoResult.Builder builder() { + return ImmutableAmmInfoResult.builder(); + } + + /** + * The AMM ledger object. + * + * @return An {@link AmmInfoResult}. + */ + @JsonProperty("amm") + AmmInfo amm(); + + /** + * (Omitted if ledger_current_index is provided instead) The ledger index of the ledger version used when + * retrieving this information. The information does not contain any changes from ledger versions newer than this one. + * + * @return An optionally-present {@link LedgerIndex}. + */ + @JsonProperty("ledger_index") + Optional ledgerIndex(); + + /** + * Get {@link #ledgerIndex()}, or throw an {@link IllegalStateException} if {@link #ledgerIndex()} is empty. + * + * @return The value of {@link #ledgerIndex()}. + * @throws IllegalStateException If {@link #ledgerIndex()} is empty. + */ + @JsonIgnore + @Value.Auxiliary + default LedgerIndex ledgerIndexSafe() { + return ledgerIndex() + .orElseThrow(() -> new IllegalStateException("Result did not contain a ledgerIndex.")); + } + + /** + * (Omitted if ledger_index is provided instead) The ledger index of the current in-progress ledger, + * which was used when retrieving this information. + * + * @return An optionally-present {@link LedgerIndex}. + */ + @JsonProperty("ledger_current_index") + Optional ledgerCurrentIndex(); + + /** + * Get {@link #ledgerCurrentIndex()}, or throw an {@link IllegalStateException} if {@link #ledgerCurrentIndex()} is + * empty. + * + * @return The value of {@link #ledgerCurrentIndex()}. + * @throws IllegalStateException If {@link #ledgerCurrentIndex()} is empty. + */ + @JsonIgnore + @Value.Auxiliary + default LedgerIndex ledgerCurrentIndexSafe() { + return ledgerCurrentIndex() + .orElseThrow(() -> new IllegalStateException("Result did not contain a ledgerCurrentIndex.")); + } + + /** + * True if this data is from a validated ledger version; if false, this data is not final. + * + * @return {@code true} if this data is from a validated ledger version, otherwise {@code false}. + */ + @Value.Default + default boolean validated() { + return false; + } + + /** + * The identifying Hash of the ledger version used to generate this response. + * + * @return A {@link Hash256} containing the ledger hash. + */ + @JsonProperty("ledger_hash") + Optional ledgerHash(); + + /** + * Get {@link #ledgerHash()}, or throw an {@link IllegalStateException} if {@link #ledgerHash()} is empty. + * + * @return The value of {@link #ledgerHash()}. + * @throws IllegalStateException If {@link #ledgerHash()} is empty. + */ + @JsonIgnore + @Value.Auxiliary + default Hash256 ledgerHashSafe() { + return ledgerHash() + .orElseThrow(() -> new IllegalStateException("Result did not contain a ledgerHash.")); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoVoteEntry.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoVoteEntry.java new file mode 100644 index 000000000..020de79e8 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoVoteEntry.java @@ -0,0 +1,58 @@ +package org.xrpl.xrpl4j.model.client.amm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.ledger.ImmutableVoteEntry; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.TradingFee; +import org.xrpl.xrpl4j.model.transactions.VoteWeight; + +/** + * Describes a vote for the trading fee on an AMM by an LP. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmInfoVoteEntry.class) +@JsonDeserialize(as = ImmutableAmmInfoVoteEntry.class) +@Beta +public interface AmmInfoVoteEntry { + + /** + * Construct a {@code AmmInfoVoteEntry} builder. + * + * @return An {@link ImmutableAmmInfoVoteEntry.Builder}. + */ + static ImmutableAmmInfoVoteEntry.Builder builder() { + return ImmutableAmmInfoVoteEntry.builder(); + } + + /** + * The address of the LP who voted. + * + * @return An {@link Address}. + */ + @JsonProperty("account") + Address account(); + + /** + * The trading fee that the LP voted for. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("trading_fee") + TradingFee tradingFee(); + + /** + * The weight of the LP's vote. + * + * @return The {@link VoteWeight}. + */ + @JsonProperty("vote_weight") + VoteWeight voteWeight(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/AmmLedgerEntryParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/AmmLedgerEntryParams.java new file mode 100644 index 000000000..e9accab4e --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/AmmLedgerEntryParams.java @@ -0,0 +1,40 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.ledger.Issue; + +/** + * Parameters that uniquely identify an {@link org.xrpl.xrpl4j.model.ledger.AmmObject} on ledger that can be used + * in a {@link LedgerEntryRequestParams} to request an {@link org.xrpl.xrpl4j.model.ledger.AmmObject}. + */ +@Immutable +@JsonSerialize(as = ImmutableAmmLedgerEntryParams.class) +@JsonDeserialize(as = ImmutableAmmLedgerEntryParams.class) +public interface AmmLedgerEntryParams { + + /** + * Construct a {@code AmmLedgerEntryParams} builder. + * + * @return An {@link ImmutableAmmLedgerEntryParams.Builder}. + */ + static ImmutableAmmLedgerEntryParams.Builder builder() { + return ImmutableAmmLedgerEntryParams.builder(); + } + + /** + * One of the two assets in the AMM's pool. + * + * @return An {@link Issue}. + */ + Issue asset(); + + /** + * The other of the two assets in the AMM's pool. + * + * @return An {@link Issue}. + */ + Issue asset2(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/DepositPreAuthLedgerEntryParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/DepositPreAuthLedgerEntryParams.java new file mode 100644 index 000000000..8dde10185 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/DepositPreAuthLedgerEntryParams.java @@ -0,0 +1,40 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.transactions.Address; + +/** + * Parameters that uniquely identify a {@link org.xrpl.xrpl4j.model.ledger.DepositPreAuthObject} on ledger that can be + * used in a {@link LedgerEntryRequestParams} to request an {@link org.xrpl.xrpl4j.model.ledger.DepositPreAuthObject}. + */ +@Immutable +@JsonSerialize(as = ImmutableDepositPreAuthLedgerEntryParams.class) +@JsonDeserialize(as = ImmutableDepositPreAuthLedgerEntryParams.class) +public interface DepositPreAuthLedgerEntryParams { + + /** + * Construct a {@code DepositPreAuthLedgerEntryParams} builder. + * + * @return An {@link ImmutableDepositPreAuthLedgerEntryParams.Builder}. + */ + static ImmutableDepositPreAuthLedgerEntryParams.Builder builder() { + return ImmutableDepositPreAuthLedgerEntryParams.builder(); + } + + /** + * The {@link Address} of the account that provided the preauthorization. + * + * @return An {@link Address}. + */ + Address owner(); + + /** + * The {@link Address} of the account that received the preauthorization. + * + * @return An {@link Address}. + */ + Address authorized(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/EscrowLedgerEntryParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/EscrowLedgerEntryParams.java new file mode 100644 index 000000000..1c015e654 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/EscrowLedgerEntryParams.java @@ -0,0 +1,41 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.primitives.UnsignedInteger; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.transactions.Address; + +/** + * Parameters that uniquely identify an {@link org.xrpl.xrpl4j.model.ledger.EscrowObject} on ledger that can be used in + * a {@link LedgerEntryRequestParams} to request an {@link org.xrpl.xrpl4j.model.ledger.EscrowObject}. + */ +@Immutable +@JsonSerialize(as = ImmutableEscrowLedgerEntryParams.class) +@JsonDeserialize(as = ImmutableEscrowLedgerEntryParams.class) +public interface EscrowLedgerEntryParams { + + /** + * Construct a {@code EscrowLedgerEntryParams} builder. + * + * @return An {@link ImmutableEscrowLedgerEntryParams.Builder}. + */ + static ImmutableEscrowLedgerEntryParams.Builder builder() { + return ImmutableEscrowLedgerEntryParams.builder(); + } + + /** + * The owner (sender) of the Escrow object. + * + * @return The {@link Address} of the owner. + */ + Address owner(); + + /** + * The Sequence Number of the transaction that created the Escrow object. + * + * @return An {@link UnsignedInteger}. + */ + UnsignedInteger seq(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParams.java new file mode 100644 index 000000000..9a74f558c --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParams.java @@ -0,0 +1,456 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.immutables.value.Value; +import org.immutables.value.Value.Style.BuilderVisibility; +import org.xrpl.xrpl4j.model.client.XrplRequestParams; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; +import org.xrpl.xrpl4j.model.ledger.AccountRootObject; +import org.xrpl.xrpl4j.model.ledger.AmmObject; +import org.xrpl.xrpl4j.model.ledger.CheckObject; +import org.xrpl.xrpl4j.model.ledger.DepositPreAuthObject; +import org.xrpl.xrpl4j.model.ledger.EscrowObject; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; +import org.xrpl.xrpl4j.model.ledger.NfTokenPageObject; +import org.xrpl.xrpl4j.model.ledger.OfferObject; +import org.xrpl.xrpl4j.model.ledger.PayChannelObject; +import org.xrpl.xrpl4j.model.ledger.RippleStateObject; +import org.xrpl.xrpl4j.model.ledger.TicketObject; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Hash256; + +import java.util.Optional; + +/** + * Request parameters for the {@code ledger_entry} RPC. + * + *

Unlike most other immutable objects in this library, this class's builder is not accessible to developers. + * Instead, developers should construct instances of {@link LedgerEntryRequestParams} via its various static + * constructors.

+ * + *

Each static constructor constructs {@link LedgerEntryRequestParams} for a particular type of ledger entry + * as described on xrpl.org.

+ * + * @param The type of {@link LedgerObject} that will be returned in the response to the {@code ledger_entry} RPC + * call with these {@link LedgerEntryRequestParams}. + */ +@Value.Immutable +// Note: These parameters should only be constructed via the provided static constructors. Exposing the builder to +// developers allows them to specify multiple types of ledger_entry request, which is unsafe to do. +@Value.Style(builderVisibility = BuilderVisibility.PACKAGE) +@JsonSerialize(as = ImmutableLedgerEntryRequestParams.class) +@JsonDeserialize(as = ImmutableLedgerEntryRequestParams.class) +@SuppressWarnings("OverloadMethodsDeclarationOrder") +public interface LedgerEntryRequestParams extends XrplRequestParams { + + /** + * Construct a {@link LedgerEntryRequestParams} that requests a ledger entry by index. + * + * @param id The index or ID of the ledger entry as a {@link Hash256}. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * @param ledgerObjectClass The class of {@link LedgerObject} that should be returned by rippled as a {@link Class} of + * {@link T}. + * @param The actual type of {@link LedgerObject} that should be returned by rippled. + * + * @return A {@link LedgerEntryRequestParams} for {@link T}. + */ + static LedgerEntryRequestParams index( + Hash256 id, + Class ledgerObjectClass, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .index(id) + .ledgerObjectClass(ledgerObjectClass) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests a ledger entry by index but does not narrow down the + * polymorphic type of {@link LedgerObject} that is returned. These parameters are useful when querying a ledger entry + * by ID that the developer does not know the type of at compile time. + * + * @param id The index or ID of the ledger entry as a {@link Hash256}. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link LedgerObject}. + */ + static LedgerEntryRequestParams index( + Hash256 id, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .index(id) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests an {@link AccountRootObject} ledger entry by address. + * + * @param address The {@link Address} of the account who owns the {@link AccountRootObject}. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link AccountRootObject}. + */ + static LedgerEntryRequestParams accountRoot( + Address address, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .accountRoot(address) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests an {@link AmmObject} ledger entry. + * + *

Note that although the rippled API allows you to specify either the AMM's ID or its two assets, this + * class does not allow developers to request an {@link AmmObject} by ID via this method. Instead, developers should + * use {@link LedgerEntryRequestParams#index()} and specify {@link AmmObject} as the {@code ledgerObjectClass} + * parameter.

+ * + * @param params The {@link AmmLedgerEntryParams} that uniquely identify the {@link AmmObject} on ledger. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link AmmObject}. + */ + static LedgerEntryRequestParams amm( + AmmLedgerEntryParams params, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .amm(params) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests an {@link OfferObject} ledger entry. + * + *

Note that although the rippled API allows you to specify either the Offer's ID or the account that created it + * and the sequence number of the transaction that created it, this class does not allow developers to request an + * {@link OfferObject} by ID via this method. Instead, developers should use {@link LedgerEntryRequestParams#index()} + * and specify {@link OfferObject} as the {@code ledgerObjectClass} parameter.

+ * + * @param params The {@link OfferLedgerEntryParams} that uniquely identify the {@link OfferObject} on + * ledger. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link OfferObject}. + */ + static LedgerEntryRequestParams offer( + OfferLedgerEntryParams params, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .offer(params) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests a {@link RippleStateObject} ledger entry. + * + * @param params The {@link RippleStateLedgerEntryParams} that uniquely identify the + * {@link RippleStateObject} on ledger. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link RippleStateObject}. + */ + static LedgerEntryRequestParams rippleState( + RippleStateLedgerEntryParams params, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .rippleState(params) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests a {@link CheckObject} ledger entry. + * + * @param id The index or ID of the {@link CheckObject}. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link RippleStateObject}. + */ + static LedgerEntryRequestParams check( + Hash256 id, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .check(id) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests a {@link EscrowObject} ledger entry. + * + *

Note that although the rippled API allows you to specify either the Escrow's ID or the owner and sequence + * number of the transaction that created the Escrow, this class does not allow developers to request an + * {@link EscrowObject} by ID via this method. Instead, developers should use {@link LedgerEntryRequestParams#index()} + * and specify {@link EscrowObject} as the {@code ledgerObjectClass} parameter.

+ * + * @param params The {@link EscrowLedgerEntryParams} that uniquely identify the {@link EscrowObject} on + * ledger. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link EscrowObject}. + */ + static LedgerEntryRequestParams escrow( + EscrowLedgerEntryParams params, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .escrow(params) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests a {@link PayChannelObject} ledger entry. + * + * @param id The index or ID of the {@link PayChannelObject}. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link PayChannelObject}. + */ + static LedgerEntryRequestParams paymentChannel( + Hash256 id, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .paymentChannel(id) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests a {@link DepositPreAuthObject} ledger entry. + * + *

Note that although the rippled API allows you to specify either the DepositPreAuth's ID or the owner and the + * account that is authorized, this class does not allow developers to request an {@link DepositPreAuthObject} by ID + * via this method. Instead, developers should use {@link LedgerEntryRequestParams#index()} and specify + * {@link DepositPreAuthObject} as the {@code ledgerObjectClass} parameter.

+ * + * @param params The {@link DepositPreAuthLedgerEntryParams} that uniquely identify the + * {@link DepositPreAuthObject} on ledger. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link DepositPreAuthObject}. + */ + static LedgerEntryRequestParams depositPreAuth( + DepositPreAuthLedgerEntryParams params, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .depositPreAuth(params) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests a {@link TicketObject} ledger entry. + * + *

Note that although the rippled API allows you to specify either the Ticket's ID or the owner of the Ticket and + * the Ticket's sequence, this class does not allow developers to request an {@link TicketObject} by ID via this + * method. Instead, developers should use {@link LedgerEntryRequestParams#index()} and specify {@link TicketObject} as + * the {@code ledgerObjectClass} parameter.

+ * + * @param params The {@link TicketLedgerEntryParams} that uniquely identify the {@link TicketObject} on + * ledger. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link TicketObject}. + */ + static LedgerEntryRequestParams ticket( + TicketLedgerEntryParams params, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .ticket(params) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Construct a {@link LedgerEntryRequestParams} that requests a {@link NfTokenPageObject} ledger entry. + * + * @param id The index or ID of the {@link NfTokenPageObject}. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link NfTokenPageObject}. + */ + static LedgerEntryRequestParams nftPage( + Hash256 id, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .nftPage(id) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + + /** + * Specifies the ledger version to request. A ledger version can be specified by ledger hash, numerical ledger index, + * or a shortcut value. + * + * @return A {@link LedgerSpecifier} specifying the ledger version to request. + */ + @JsonUnwrapped + LedgerSpecifier ledgerSpecifier(); + + /** + * If true, return the requested ledger entry's contents as a hex string in the XRP Ledger's binary format. Otherwise, + * return data in JSON format. This field will always be {@code false}. + * + * @return A boolean. + */ + @Value.Derived + default boolean binary() { + return false; + } + + /** + * Look up a ledger entry by ID/index. + * + * @return An optionally-present {@link Hash256}. + */ + Optional index(); + + /** + * Look up an {@link org.xrpl.xrpl4j.model.ledger.AccountRootObject} by {@link Address}. + * + * @return An optionally-present {@link Address}. + */ + @JsonProperty("account_root") + Optional
accountRoot(); + + /** + * Look up an {@link org.xrpl.xrpl4j.model.ledger.AmmObject} by {@link AmmLedgerEntryParams}. + * + * @return An optionally-present {@link AmmLedgerEntryParams}. + */ + Optional amm(); + + /** + * Look up an {@link org.xrpl.xrpl4j.model.ledger.OfferObject} by {@link OfferLedgerEntryParams}. + * + * @return An optionally-present {@link OfferLedgerEntryParams}. + */ + Optional offer(); + + /** + * Look up a {@link org.xrpl.xrpl4j.model.ledger.RippleStateObject} by {@link RippleStateLedgerEntryParams}. + * + * @return An optionally-present {@link RippleStateLedgerEntryParams}. + */ + @JsonProperty("ripple_state") + Optional rippleState(); + + /** + * Look up a {@link org.xrpl.xrpl4j.model.ledger.CheckObject} by ID. + * + * @return An optionally-present {@link Hash256}. + */ + Optional check(); + + /** + * Look up an {@link org.xrpl.xrpl4j.model.ledger.EscrowObject} by {@link EscrowLedgerEntryParams}. + * + * @return An optionally-present {@link EscrowLedgerEntryParams}. + */ + Optional escrow(); + + /** + * Look up a {@link org.xrpl.xrpl4j.model.ledger.PayChannelObject} by ID. + * + * @return An optionally-present {@link Hash256}. + */ + @JsonProperty("payment_channel") + Optional paymentChannel(); + + /** + * Look up an {@link org.xrpl.xrpl4j.model.ledger.NfTokenPageObject} by ID. + * + * @return An optionally-present {@link Hash256}. + */ + @JsonProperty("nft_page") + Optional nftPage(); + + /** + * Look up a {@link org.xrpl.xrpl4j.model.ledger.DepositPreAuthObject} by {@link DepositPreAuthLedgerEntryParams}. + * + * @return An optionally-present {@link DepositPreAuthLedgerEntryParams}. + */ + @JsonProperty("deposit_preauth") + Optional depositPreAuth(); + + /** + * Look up a {@link org.xrpl.xrpl4j.model.ledger.TicketObject} by {@link TicketLedgerEntryParams}. + * + * @return An optionally-present {@link TicketLedgerEntryParams}. + */ + Optional ticket(); + + /** + * The {@link Class} of {@link T}. This field is helpful when telling Jackson how to deserialize rippled's response to + * a {@link T}. + * + * @return A {@link Class} of type {@link T}. + */ + @JsonIgnore + @Value.Default + default Class ledgerObjectClass() { + if (accountRoot().isPresent()) { + return (Class) AccountRootObject.class; + } + + if (amm().isPresent()) { + return (Class) AmmObject.class; + } + + if (offer().isPresent()) { + return (Class) OfferObject.class; + } + + if (rippleState().isPresent()) { + return (Class) RippleStateObject.class; + } + + if (check().isPresent()) { + return (Class) CheckObject.class; + } + + if (escrow().isPresent()) { + return (Class) EscrowObject.class; + } + + if (paymentChannel().isPresent()) { + return (Class) PayChannelObject.class; + } + + if (nftPage().isPresent()) { + return (Class) NfTokenPageObject.class; + } + + if (depositPreAuth().isPresent()) { + return (Class) DepositPreAuthObject.class; + } + + if (ticket().isPresent()) { + return (Class) TicketObject.class; + } + + return (Class) LedgerObject.class; + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResult.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResult.java new file mode 100644 index 000000000..49d71c663 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResult.java @@ -0,0 +1,126 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +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 org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.client.XrplResult; +import org.xrpl.xrpl4j.model.client.common.LedgerIndex; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; +import org.xrpl.xrpl4j.model.transactions.Hash256; + +import java.util.Optional; + +/** + * The result of a {@code ledger_entry} RPC call. + * + * @param The type of {@link LedgerObject} contained in the result. + */ +@Immutable +@JsonSerialize(as = ImmutableLedgerEntryResult.class) +@JsonDeserialize(as = ImmutableLedgerEntryResult.class) +public interface LedgerEntryResult extends XrplResult { + + /** + * Construct a {@code LedgerEntryResult} builder. + * + * @return An {@link ImmutableLedgerEntryResult.Builder}. + */ + static ImmutableLedgerEntryResult.Builder builder() { + return ImmutableLedgerEntryResult.builder(); + } + + /** + * The ledger entry returned, as a {@link T}. + * + * @return A {@link T}. + */ + T node(); + + /** + * Unique identifying hash of the entire ledger. + * + * @return A {@link Hash256} containing the ledger hash. + */ + @JsonProperty("ledger_hash") + Optional ledgerHash(); + + /** + * Get {@link #ledgerHash()}, or throw an {@link IllegalStateException} if {@link #ledgerHash()} is empty. + * + * @return The value of {@link #ledgerHash()}. + * + * @throws IllegalStateException If {@link #ledgerHash()} is empty. + */ + @JsonIgnore + @Value.Auxiliary + default Hash256 ledgerHashSafe() { + return ledgerHash() + .orElseThrow(() -> new IllegalStateException("Result did not contain a ledgerHash.")); + } + + /** + * The {@link LedgerIndex} of this ledger. + * + * @return The {@link LedgerIndex} of this ledger. + */ + @JsonProperty("ledger_index") + Optional ledgerIndex(); + + /** + * Get {@link #ledgerIndex()}, or throw an {@link IllegalStateException} if {@link #ledgerIndex()} is empty. + * + * @return The value of {@link #ledgerIndex()}. + * + * @throws IllegalStateException If {@link #ledgerIndex()} is empty. + */ + @JsonIgnore + @Value.Auxiliary + default LedgerIndex ledgerIndexSafe() { + return ledgerIndex() + .orElseThrow(() -> new IllegalStateException("Result did not contain a ledgerIndex.")); + } + + /** + * The {@link LedgerIndex} of this ledger, if the ledger is the current ledger. Only present on a current ledger + * response. + * + * @return A {@link LedgerIndex} if this result is for the current ledger, otherwise {@link Optional#empty()}. + */ + @JsonProperty("ledger_current_index") + Optional ledgerCurrentIndex(); + + /** + * Get {@link #ledgerCurrentIndex()}, or throw an {@link IllegalStateException} if {@link #ledgerCurrentIndex()} is + * empty. + * + * @return The value of {@link #ledgerCurrentIndex()}. + * + * @throws IllegalStateException If {@link #ledgerCurrentIndex()} is empty. + */ + @JsonIgnore + @Value.Auxiliary + default LedgerIndex ledgerCurrentIndexSafe() { + return ledgerCurrentIndex() + .orElseThrow(() -> new IllegalStateException("Result did not contain a ledgerCurrentIndex.")); + } + + /** + * True if this data is from a validated ledger version; if false, this data is not final. + * + * @return {@code true} if this data is from a validated ledger version, otherwise {@code false}. + */ + @Value.Default + default boolean validated() { + return false; + } + + /** + * The ID of the ledger entry returned. + * + * @return The {@link Hash256} representing the ID of the ledger entry. + */ + Hash256 index(); +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/OfferLedgerEntryParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/OfferLedgerEntryParams.java new file mode 100644 index 000000000..8227de2ba --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/OfferLedgerEntryParams.java @@ -0,0 +1,41 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.primitives.UnsignedInteger; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.transactions.Address; + +/** + * Parameters that uniquely identify an {@link org.xrpl.xrpl4j.model.ledger.OfferObject} on ledger that can be used in a + * {@link LedgerEntryRequestParams} to request an {@link org.xrpl.xrpl4j.model.ledger.OfferObject}. + */ +@Immutable +@JsonSerialize(as = ImmutableOfferLedgerEntryParams.class) +@JsonDeserialize(as = ImmutableOfferLedgerEntryParams.class) +public interface OfferLedgerEntryParams { + + /** + * Construct a {@code OfferLedgerEntryParams} builder. + * + * @return An {@link ImmutableOfferLedgerEntryParams.Builder}. + */ + static ImmutableOfferLedgerEntryParams.Builder builder() { + return ImmutableOfferLedgerEntryParams.builder(); + } + + /** + * The account that placed the offer. + * + * @return The {@link Address} of the account. + */ + Address account(); + + /** + * The Sequence Number of the transaction that created the Offer entry. + * + * @return An {@link UnsignedInteger}. + */ + UnsignedInteger seq(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/RippleStateLedgerEntryParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/RippleStateLedgerEntryParams.java new file mode 100644 index 000000000..7824b388d --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/RippleStateLedgerEntryParams.java @@ -0,0 +1,78 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.transactions.Address; + +import java.util.List; + +/** + * Parameters that uniquely identify a {@link org.xrpl.xrpl4j.model.ledger.RippleStateObject} on ledger that can be used + * in a {@link LedgerEntryRequestParams} to request a {@link org.xrpl.xrpl4j.model.ledger.RippleStateObject}. + */ +@Immutable +@JsonSerialize(as = ImmutableRippleStateLedgerEntryParams.class) +@JsonDeserialize(as = ImmutableRippleStateLedgerEntryParams.class) +public interface RippleStateLedgerEntryParams { + + /** + * Construct a {@code RippleStateLedgerEntryParams} builder. + * + * @return An {@link ImmutableRippleStateLedgerEntryParams.Builder}. + */ + static ImmutableRippleStateLedgerEntryParams.Builder builder() { + return ImmutableRippleStateLedgerEntryParams.builder(); + } + + /** + * A {@link RippleStateAccounts} containing the two accounts linked by the + * {@link org.xrpl.xrpl4j.model.ledger.RippleStateObject}. + * + * @return A {@link RippleStateAccounts}. + */ + @JsonUnwrapped + RippleStateAccounts accounts(); + + /** + * The currency code of the {@link org.xrpl.xrpl4j.model.ledger.RippleStateObject} to retrieve. + * + * @return A {@link String}. + */ + String currency(); + + /** + * Specifies two {@link Address}es of accounts that are linked by a + * {@link org.xrpl.xrpl4j.model.ledger.RippleStateObject}. + */ + @Immutable + @JsonSerialize(as = ImmutableRippleStateAccounts.class) + @JsonDeserialize(as = ImmutableRippleStateAccounts.class) + interface RippleStateAccounts { + + /** + * Construct a new {@link RippleStateAccounts}. + * + * @param account The {@link Address} of one of the accounts linked in the object. + * @param otherAccount The {@link Address} of the other account linked in the object. + * + * @return A {@link RippleStateAccounts}. + */ + static RippleStateAccounts of(Address account, Address otherAccount) { + return ImmutableRippleStateAccounts.builder() + .addAccounts(account, otherAccount) + .build(); + } + + /** + * The {@link Address}es of the accounts linked by the {@link org.xrpl.xrpl4j.model.ledger.RippleStateObject}. + * + *

Note that this is typed as a {@link List} so that this object is serialized as a JSON array.

+ * + * @return A {@link List} of {@link Address}es. + */ + List
accounts(); + + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/TicketLedgerEntryParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/TicketLedgerEntryParams.java new file mode 100644 index 000000000..67d29c44b --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/TicketLedgerEntryParams.java @@ -0,0 +1,43 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +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.UnsignedInteger; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.transactions.Address; + +/** + * Parameters that uniquely identify a {@link org.xrpl.xrpl4j.model.ledger.TicketObject} on ledger that can be used in a + * {@link LedgerEntryRequestParams} to request a {@link org.xrpl.xrpl4j.model.ledger.TicketObject}. + */ +@Immutable +@JsonSerialize(as = ImmutableTicketLedgerEntryParams.class) +@JsonDeserialize(as = ImmutableTicketLedgerEntryParams.class) +public interface TicketLedgerEntryParams { + + /** + * Construct a {@code TicketLedgerEntryParams} builder. + * + * @return An {@link ImmutableTicketLedgerEntryParams.Builder}. + */ + static ImmutableTicketLedgerEntryParams.Builder builder() { + return ImmutableTicketLedgerEntryParams.builder(); + } + + /** + * The owner of the Ticket. + * + * @return The {@link Address} of the owner. + */ + Address account(); + + /** + * The Ticket Sequence number of the Ticket to retrieve. + * + * @return An {@link UnsignedInteger}. + */ + @JsonProperty("ticket_seq") + UnsignedInteger ticketSeq(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AccountRootFlags.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AccountRootFlags.java index 0df9b28ba..ca841206f 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AccountRootFlags.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AccountRootFlags.java @@ -20,6 +20,7 @@ * =========================LICENSE_END================================== */ +import com.google.common.annotations.Beta; import org.xrpl.xrpl4j.model.transactions.AccountSet; /** @@ -97,6 +98,15 @@ public class AccountRootFlags extends Flags { */ public static final AccountRootFlags DISALLOW_INCOMING_TRUSTLINE = new AccountRootFlags(0x20000000); + /** + * Constant {@link AccountRootFlags} for the {@code lsfAllowTrustLineClawback} account flag. + * + *

This constant will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject + * to change.

+ */ + @Beta + public static final AccountRootFlags ALLOW_TRUSTLINE_CLAWBACK = new AccountRootFlags(0x80000000L); + /** * Required-args Constructor. * @@ -110,6 +120,7 @@ private AccountRootFlags(final long value) { * Construct {@link AccountRootFlags} with a given value. * * @param value The long-number encoded flags value of this {@link AccountRootFlags}. + * * @return New {@link AccountRootFlags}. */ public static AccountRootFlags of(long value) { @@ -235,4 +246,17 @@ public boolean lsfDisallowIncomingPayChan() { public boolean lsfDisallowIncomingTrustline() { return this.isSet(AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE); } + + /** + * Allows trustline clawback on this account. + * + *

This constant will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject + * to change.

+ * + * @return {@code true} if {@code lsfAllowTrustLineClawback} is set, otherwise {@code false}. + */ + @Beta + public boolean lsfAllowTrustLineClawback() { + return this.isSet(AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK); + } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AmmDepositFlags.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AmmDepositFlags.java new file mode 100644 index 000000000..570c2b903 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AmmDepositFlags.java @@ -0,0 +1,108 @@ +package org.xrpl.xrpl4j.model.flags; + +import com.google.common.annotations.Beta; +import org.xrpl.xrpl4j.model.transactions.AmmDeposit; + +/** + * A set of {@link TransactionFlags} that can be set on {@link AmmDeposit} transactions. Exactly one flag must be set on + * each {@link AmmDeposit} transaction, so this class does not allow for combination of multiple flags. + * + *

While most other TransactionFlags support empty flags or 0, AmmDeposit transactions must have a Flags field + * to denote the deposit mode. Therefore, AmmDepositFlags does not support empty or unset flags. + *

+ * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Beta +public class AmmDepositFlags extends TransactionFlags { + + /** + * Constant {@link AmmDepositFlags} for the {@code tfLPToken} flag. + */ + public static final AmmDepositFlags LP_TOKEN = new AmmDepositFlags(0x00010000); + + /** + * Constant {@link AmmDepositFlags} for the {@code tfSingleAsset} flag. + */ + public static final AmmDepositFlags SINGLE_ASSET = new AmmDepositFlags(0x00080000); + + /** + * Constant {@link AmmDepositFlags} for the {@code tfTwoAsset} flag. + */ + public static final AmmDepositFlags TWO_ASSET = new AmmDepositFlags(0x00100000); + + /** + * Constant {@link AmmDepositFlags} for the {@code tfOneAssetLPToken} flag. + */ + public static final AmmDepositFlags ONE_ASSET_LP_TOKEN = new AmmDepositFlags(0x00200000); + + /** + * Constant {@link AmmDepositFlags} for the {@code tfLimitLPToken} flag. + */ + public static final AmmDepositFlags LIMIT_LP_TOKEN = new AmmDepositFlags(0x00400000); + + /** + * Constant {@link AmmDepositFlags} for the {@code tfTwoAssetIfEmpty} flag. + */ + public static final AmmDepositFlags TWO_ASSET_IF_EMPTY = new AmmDepositFlags(0x00800000); + + private AmmDepositFlags(long value) { + super(value); + } + + /** + * Whether the {@code tfLPToken} flag is set. + * + * @return {@code true} if {@code tfLPToken} is set, otherwise {@code false}. + */ + public boolean tfLpToken() { + return this.isSet(LP_TOKEN); + } + + /** + * Whether the {@code tfSingleAsset} flag is set. + * + * @return {@code true} if {@code tfSingleAsset} is set, otherwise {@code false}. + */ + public boolean tfSingleAsset() { + return this.isSet(SINGLE_ASSET); + } + + /** + * Whether the {@code tfTwoAsset} flag is set. + * + * @return {@code true} if {@code tfTwoAsset} is set, otherwise {@code false}. + */ + public boolean tfTwoAsset() { + return this.isSet(TWO_ASSET); + } + + /** + * Whether the {@code tfOneAssetLPToken} flag is set. + * + * @return {@code true} if {@code tfOneAssetLPToken} is set, otherwise {@code false}. + */ + public boolean tfOneAssetLpToken() { + return this.isSet(ONE_ASSET_LP_TOKEN); + } + + /** + * Whether the {@code tfLimitLPToken} flag is set. + * + * @return {@code true} if {@code tfLimitLPToken} is set, otherwise {@code false}. + */ + public boolean tfLimitLpToken() { + return this.isSet(LIMIT_LP_TOKEN); + } + + /** + * Whether the {@code tfTwoAssetIfEmpty} flag is set. + * + * @return {@code true} if {@code tfTwoAssetIfEmpty} is set, otherwise {@code false}. + */ + public boolean tfTwoAssetIfEmpty() { + return this.isSet(TWO_ASSET_IF_EMPTY); + } + +} \ No newline at end of file diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AmmWithdrawFlags.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AmmWithdrawFlags.java new file mode 100644 index 000000000..dfdc55bb7 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AmmWithdrawFlags.java @@ -0,0 +1,121 @@ +package org.xrpl.xrpl4j.model.flags; + +import com.google.common.annotations.Beta; + +/** + * A set of {@link TransactionFlags} that can be set on {@link org.xrpl.xrpl4j.model.transactions.AmmWithdraw} + * transactions. Exactly one flag must be set on each {@link org.xrpl.xrpl4j.model.transactions.AmmWithdraw} + * transaction, so this class does not allow for combination of multiple flags. + * + *

While most other TransactionFlags support empty flags or 0, AmmWithdraw transactions must have a Flags field + * to denote the withdraw mode. Therefore, AmmWithdrawFlags does not support empty or unset flags. + *

+ * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Beta +public class AmmWithdrawFlags extends TransactionFlags { + + /** + * Constant {@link AmmWithdrawFlags} for the {@code tfLPToken} flag. + */ + public static final AmmWithdrawFlags LP_TOKEN = new AmmWithdrawFlags(0x00010000); + + /** + * Constant {@link AmmWithdrawFlags} for the {@code tfWithdrawAll} flag. + */ + public static final AmmWithdrawFlags WITHDRAW_ALL = new AmmWithdrawFlags(0x00020000); + + /** + * Constant {@link AmmWithdrawFlags} for the {@code tfOneAssetWithdrawAll} flag. + */ + public static final AmmWithdrawFlags ONE_ASSET_WITHDRAW_ALL = new AmmWithdrawFlags(0x00040000); + + /** + * Constant {@link AmmWithdrawFlags} for the {@code tfSingleAsset} flag. + */ + public static final AmmWithdrawFlags SINGLE_ASSET = new AmmWithdrawFlags(0x00080000); + + /** + * Constant {@link AmmWithdrawFlags} for the {@code tfTwoAsset} flag. + */ + public static final AmmWithdrawFlags TWO_ASSET = new AmmWithdrawFlags(0x00100000); + + /** + * Constant {@link AmmWithdrawFlags} for the {@code tfOneAssetLPToken} flag. + */ + public static final AmmWithdrawFlags ONE_ASSET_LP_TOKEN = new AmmWithdrawFlags(0x00200000); + + /** + * Constant {@link AmmWithdrawFlags} for the {@code tfLimitLPToken} flag. + */ + public static final AmmWithdrawFlags LIMIT_LP_TOKEN = new AmmWithdrawFlags(0x00400000); + + private AmmWithdrawFlags(long value) { + super(value); + } + + /** + * Whether the {@code tfLPToken} flag is set. + * + * @return {@code true} if {@code tfLPToken} is set, otherwise {@code false}. + */ + public boolean tfLpToken() { + return this.isSet(LP_TOKEN); + } + + /** + * Whether the {@code tfWithdrawAll} flag is set. + * + * @return {@code true} if {@code tfWithdrawAll} is set, otherwise {@code false}. + */ + public boolean tfWithdrawAll() { + return this.isSet(WITHDRAW_ALL); + } + + /** + * Whether the {@code tfOneAssetWithdrawAll} flag is set. + * + * @return {@code true} if {@code tfOneAssetWithdrawAll} is set, otherwise {@code false}. + */ + public boolean tfOneAssetWithdrawAll() { + return this.isSet(ONE_ASSET_WITHDRAW_ALL); + } + + /** + * Whether the {@code tfSingleAsset} flag is set. + * + * @return {@code true} if {@code tfSingleAsset} is set, otherwise {@code false}. + */ + public boolean tfSingleAsset() { + return this.isSet(SINGLE_ASSET); + } + + /** + * Whether the {@code tfTwoAsset} flag is set. + * + * @return {@code true} if {@code tfTwoAsset} is set, otherwise {@code false}. + */ + public boolean tfTwoAsset() { + return this.isSet(TWO_ASSET); + } + + /** + * Whether the {@code tfOneAssetLPToken} flag is set. + * + * @return {@code true} if {@code tfOneAssetLPToken} is set, otherwise {@code false}. + */ + public boolean tfOneAssetLpToken() { + return this.isSet(ONE_ASSET_LP_TOKEN); + } + + /** + * Whether the {@code tfLimitLPToken} flag is set. + * + * @return {@code true} if {@code tfLimitLPToken} is set, otherwise {@code false}. + */ + public boolean tfLimitLpToken() { + return this.isSet(LIMIT_LP_TOKEN); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AffectedNodeDeserializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AffectedNodeDeserializer.java index 25cde0d03..da326cd40 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AffectedNodeDeserializer.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AffectedNodeDeserializer.java @@ -31,6 +31,7 @@ import org.xrpl.xrpl4j.model.transactions.metadata.CreatedNode; import org.xrpl.xrpl4j.model.transactions.metadata.DeletedNode; import org.xrpl.xrpl4j.model.transactions.metadata.MetaAccountRootObject; +import org.xrpl.xrpl4j.model.transactions.metadata.MetaAmmObject; import org.xrpl.xrpl4j.model.transactions.metadata.MetaCheckObject; import org.xrpl.xrpl4j.model.transactions.metadata.MetaDepositPreAuthObject; import org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject; @@ -117,6 +118,8 @@ private Class determineLedgerObjectType(String ledge return MetaTicketObject.class; case "NFTokenPage": return MetaNfTokenPageObject.class; + case "AMM": + return MetaAmmObject.class; default: return MetaUnknownObject.class; } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/TradingFeeDeserializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/TradingFeeDeserializer.java new file mode 100644 index 000000000..75ee1f426 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/TradingFeeDeserializer.java @@ -0,0 +1,27 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.google.common.primitives.UnsignedInteger; +import org.xrpl.xrpl4j.model.transactions.TradingFee; + +import java.io.IOException; + +/** + * Custom Jackson deserializer for {@link TradingFee}s. + */ +public class TradingFeeDeserializer extends StdDeserializer { + + /** + * No-args constructor. + */ + public TradingFeeDeserializer() { + super(TradingFee.class); + } + + @Override + public TradingFee deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { + return TradingFee.of(UnsignedInteger.valueOf(jsonParser.getLongValue())); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/TradingFeeSerializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/TradingFeeSerializer.java new file mode 100644 index 000000000..98342fef2 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/TradingFeeSerializer.java @@ -0,0 +1,26 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; +import org.xrpl.xrpl4j.model.transactions.TradingFee; + +import java.io.IOException; + +/** + * Custom Jackson serializer for {@link TradingFee}s. + */ +public class TradingFeeSerializer extends StdScalarSerializer { + + /** + * No-args constructor. + */ + public TradingFeeSerializer() { + super(TradingFee.class, false); + } + + @Override + public void serialize(TradingFee tradingFee, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeNumber(tradingFee.value().longValue()); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/VoteWeightDeserializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/VoteWeightDeserializer.java new file mode 100644 index 000000000..9e3c3d345 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/VoteWeightDeserializer.java @@ -0,0 +1,27 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.google.common.primitives.UnsignedInteger; +import org.xrpl.xrpl4j.model.transactions.VoteWeight; + +import java.io.IOException; + +/** + * Custom Jackson deserializer for {@link VoteWeight}s. + */ +public class VoteWeightDeserializer extends StdDeserializer { + + /** + * No-args constructor. + */ + public VoteWeightDeserializer() { + super(VoteWeight.class); + } + + @Override + public VoteWeight deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { + return VoteWeight.of(UnsignedInteger.valueOf(jsonParser.getLongValue())); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/VoteWeightSerializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/VoteWeightSerializer.java new file mode 100644 index 000000000..a71119b7f --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/VoteWeightSerializer.java @@ -0,0 +1,26 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; +import org.xrpl.xrpl4j.model.transactions.VoteWeight; + +import java.io.IOException; + +/** + * Custom Jackson serializer for {@link VoteWeight}s. + */ +public class VoteWeightSerializer extends StdScalarSerializer { + + /** + * No-args constructor. + */ + public VoteWeightSerializer() { + super(VoteWeight.class, false); + } + + @Override + public void serialize(VoteWeight voteWeight, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeNumber(voteWeight.value().longValue()); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AccountRootObject.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AccountRootObject.java index ba1c7e328..3639c6562 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AccountRootObject.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AccountRootObject.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; import com.google.common.primitives.UnsignedInteger; import org.immutables.value.Value; import org.xrpl.xrpl4j.model.flags.AccountRootFlags; @@ -225,6 +226,19 @@ default LedgerEntryType ledgerEntryType() { @JsonProperty("NFTokenMinter") Optional
nfTokenMinter(); + /** + * The ledger entry ID of the corresponding AMM ledger entry. Set during account creation; cannot be modified. + * If present, indicates that this is a special AMM AccountRoot; always omitted on non-AMM accounts. + * + *

This method will be marked {@link com.google.common.annotations.Beta} until the AMM amendment is enabled on + * mainnet. Its API is subject to change.

+ * + * @return An optionally-present {@link Hash256}. + */ + @Beta + @JsonProperty("AMMID") + Optional ammId(); + /** * The unique ID of this {@link AccountRootObject} ledger object. * diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AmmObject.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AmmObject.java new file mode 100644 index 000000000..3466b047c --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AmmObject.java @@ -0,0 +1,152 @@ +package org.xrpl.xrpl4j.model.ledger; + +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.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.flags.Flags; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Hash256; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TradingFee; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Represents an AMM ledger object, which describes a single Automated Market Maker instance. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmObject.class) +@JsonDeserialize(as = ImmutableAmmObject.class) +@Beta +public interface AmmObject extends LedgerObject { + + /** + * Construct a {@code AmmObject} builder. + * + * @return An {@link ImmutableAmmObject.Builder}. + */ + static ImmutableAmmObject.Builder builder() { + return ImmutableAmmObject.builder(); + } + + /** + * The type of ledger object, which will always be "AMM" in this case. + * + * @return Always returns {@link org.xrpl.xrpl4j.model.ledger.LedgerObject.LedgerEntryType#AMM}. + */ + @JsonProperty("LedgerEntryType") + @Value.Derived + default LedgerEntryType ledgerEntryType() { + return LedgerEntryType.AMM; + } + + /** + * A bit-map of boolean flags. No flags are defined for {@link AmmObject}, so this value is always 0. + * + * @return Always {@link Flags#UNSET}. + */ + @JsonProperty("Flags") + @Value.Derived + default Flags flags() { + return Flags.UNSET; + } + + /** + * The definition for one of the two assets this AMM holds. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset") + Issue asset(); + + /** + * The definition for the other asset this AMM holds. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset2") + Issue asset2(); + + /** + * The address of the special account that holds this AMM's assets. + * + * @return An {@link Address}. + */ + @JsonProperty("Account") + Address account(); + + /** + * Details of the current owner of the auction slot. + * + * @return An {@link AuctionSlot}. + */ + @JsonProperty("AuctionSlot") + Optional auctionSlot(); + + /** + * The total outstanding balance of liquidity provider tokens from this AMM instance. The holders of these tokens can + * vote on the AMM's trading fee in proportion to their holdings, or redeem the tokens for a share of the AMM's assets + * which grows with the trading fees collected. + * + * @return An {@link IssuedCurrencyAmount}. + */ + @JsonProperty("LPTokenBalance") + IssuedCurrencyAmount lpTokenBalance(); + + /** + * The percentage fee to be charged for trades against this AMM instance, in units of 1/10,000. The maximum value is + * 1000, for a 1% fee. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("TradingFee") + TradingFee tradingFee(); + + /** + * A list of vote objects, representing votes on the pool's trading fee. + * + * @return A {@link List} of {@link VoteEntryWrapper}s. + */ + @JsonProperty("VoteSlots") + List voteSlots(); + + /** + * A hint indicating which page of the sender's owner directory links to this object, in case the directory consists + * of multiple pages. + * + *

Note: The object does not contain a direct link to the owner directory containing it, since that value can be + * derived from the Account. + * + * @return A {@link String} containing the owner node hint. + */ + @JsonProperty("OwnerNode") + String ownerNode(); + + /** + * Unwraps the {@link VoteEntryWrapper}s in {@link #voteSlots()} for easier access to {@link VoteEntry}s. + * + * @return A {@link List} of {@link VoteEntry}. + */ + @JsonIgnore + @Value.Derived + default List voteSlotsUnwrapped() { + return voteSlots().stream() + .map(VoteEntryWrapper::voteEntry) + .collect(Collectors.toList()); + } + + /** + * The unique ID of the {@link AmmObject}. + * + * @return A {@link Hash256}. + */ + Hash256 index(); +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AuctionSlot.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AuctionSlot.java new file mode 100644 index 000000000..0deb2c1f2 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AuctionSlot.java @@ -0,0 +1,94 @@ +package org.xrpl.xrpl4j.model.ledger; + +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.annotations.Beta; +import com.google.common.primitives.UnsignedInteger; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TradingFee; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Represents an AuctionSlot object in an {@link AmmObject}, containing details of the current owner of the auction + * slot. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAuctionSlot.class) +@JsonDeserialize(as = ImmutableAuctionSlot.class) +@Beta +public interface AuctionSlot { + + /** + * Construct a {@code AuctionSlot} builder. + * + * @return An {@link ImmutableAuctionSlot.Builder}. + */ + static ImmutableAuctionSlot.Builder builder() { + return ImmutableAuctionSlot.builder(); + } + + /** + * The current owner of this auction slot. + * + * @return An {@link Address}. + */ + @JsonProperty("Account") + Address account(); + + /** + * A list of at most 4 additional accounts that are authorized to trade at the discounted fee for this AMM instance. + * + * @return A {@link List} of {@link AuthAccountWrapper}s. + */ + @JsonProperty("AuthAccounts") + List authAccounts(); + + /** + * Extracts all the addresses found in the {@link AuthAccount}s found in {@link #authAccounts()}. + * + * @return A {@link List} of {@link Address}. + */ + @JsonIgnore + @Value.Derived + default List
authAccountsAddresses() { + return authAccounts().stream() + .map(AuthAccountWrapper::authAccount) + .map(AuthAccount::account) + .collect(Collectors.toList()); + } + + /** + * The trading fee to be charged to the auction owner. By default this is 0, meaning that the auction owner can trade + * at no fee instead of the standard fee for this AMM. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("DiscountedFee") + TradingFee discountedFee(); + + /** + * The amount the auction owner paid to win this slot, in LP Tokens. + * + * @return An {@link IssuedCurrencyAmount}. + */ + @JsonProperty("Price") + IssuedCurrencyAmount price(); + + /** + * The time when this slot expires, in seconds since the Ripple Epoch. + * + * @return An {@link UnsignedInteger} + */ + @JsonProperty("Expiration") + UnsignedInteger expiration(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AuthAccount.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AuthAccount.java new file mode 100644 index 000000000..402d9d9d0 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AuthAccount.java @@ -0,0 +1,43 @@ +package org.xrpl.xrpl4j.model.ledger; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.transactions.Address; + +/** + * An account that is authorized to trade at the discounted fee for an AMM instance. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAuthAccount.class) +@JsonDeserialize(as = ImmutableAuthAccount.class) +@Beta +public interface AuthAccount { + + /** + * Construct an {@link AuthAccount} containing the specified {@link Address}. + * + * @param account An {@link Address}. + * + * @return An {@link AuthAccount}. + */ + static AuthAccount of(Address account) { + return ImmutableAuthAccount.builder() + .account(account) + .build(); + } + + /** + * The address of the account. + * + * @return An {@link Address}. + */ + @JsonProperty("Account") + Address account(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AuthAccountWrapper.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AuthAccountWrapper.java new file mode 100644 index 000000000..e9fc3ea63 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/AuthAccountWrapper.java @@ -0,0 +1,42 @@ +package org.xrpl.xrpl4j.model.ledger; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; + +/** + * A wrapper around {@link AuthAccount}s. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAuthAccountWrapper.class) +@JsonDeserialize(as = ImmutableAuthAccountWrapper.class) +@Beta +public interface AuthAccountWrapper { + + /** + * Construct an {@link AuthAccountWrapper} containing the specified {@link AuthAccount}. + * + * @param authAccount An {@link AuthAccount}. + * + * @return An {@link AuthAccountWrapper}. + */ + static AuthAccountWrapper of(AuthAccount authAccount) { + return ImmutableAuthAccountWrapper.builder() + .authAccount(authAccount) + .build(); + } + + /** + * An {@link AuthAccount}. + * + * @return An {@link AuthAccount}. + */ + @JsonProperty("AuthAccount") + AuthAccount authAccount(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/Issue.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/Issue.java index 613cb60e1..7fadc95ac 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/Issue.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/Issue.java @@ -63,4 +63,4 @@ static ImmutableIssue.Builder builder() { */ Optional
issuer(); -} \ No newline at end of file +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/LedgerObject.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/LedgerObject.java index fd9a23619..b6b7e1b2f 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/LedgerObject.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/LedgerObject.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.annotations.Beta; /** * Market interface for XRP Ledger Objects. @@ -52,6 +53,7 @@ @JsonSubTypes.Type(value = ImmutableRippleStateObject.class, name = "RippleState"), @JsonSubTypes.Type(value = ImmutableSignerListObject.class, name = "SignerList"), @JsonSubTypes.Type(value = ImmutableTicketObject.class, name = "Ticket"), + @JsonSubTypes.Type(value = ImmutableAmmObject.class, name = "AMM"), @JsonSubTypes.Type(value = ImmutableNfTokenPageObject.class, name = "NFTokenPage"), }) // TODO: Uncomment subtypes as we implement @@ -139,7 +141,16 @@ enum LedgerEntryType { /** * The {@link LedgerEntryType} for {@code NfTokenPageObject} ledger objects. */ - NFTOKEN_PAGE("NFTokenPage"); + NFTOKEN_PAGE("NFTokenPage"), + + /** + * The {@link LedgerEntryType} for {@code AmmObject} ledger objects. + * + *

This constant will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ + @Beta + AMM("AMM"); private final String value; diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/TicketObject.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/TicketObject.java index 0a3a834fe..f103989d7 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/TicketObject.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/TicketObject.java @@ -110,4 +110,11 @@ default Flags flags() { */ @JsonProperty("TicketSequence") UnsignedInteger ticketSequence(); + + /** + * The unique ID of the {@link TicketObject}. + * + * @return A {@link Hash256}. + */ + Hash256 index(); } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/VoteEntry.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/VoteEntry.java new file mode 100644 index 000000000..3bb2fd063 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/VoteEntry.java @@ -0,0 +1,57 @@ +package org.xrpl.xrpl4j.model.ledger; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.TradingFee; +import org.xrpl.xrpl4j.model.transactions.VoteWeight; + +/** + * Describes a vote for the trading fee on an AMM by an LP. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableVoteEntry.class) +@JsonDeserialize(as = ImmutableVoteEntry.class) +@Beta +public interface VoteEntry { + + /** + * Construct a {@code VoteEntry} builder. + * + * @return An {@link ImmutableVoteEntry.Builder}. + */ + static ImmutableVoteEntry.Builder builder() { + return ImmutableVoteEntry.builder(); + } + + /** + * The address of the LP who voted. + * + * @return An {@link Address}. + */ + @JsonProperty("Account") + Address account(); + + /** + * The trading fee that the LP voted for. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("TradingFee") + TradingFee tradingFee(); + + /** + * The weight of the LP's vote. + * + * @return The {@link VoteWeight}. + */ + @JsonProperty("VoteWeight") + VoteWeight voteWeight(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/VoteEntryWrapper.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/VoteEntryWrapper.java new file mode 100644 index 000000000..70a058775 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/VoteEntryWrapper.java @@ -0,0 +1,43 @@ +package org.xrpl.xrpl4j.model.ledger; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; + +/** + * A wrapper around a {@link VoteEntry}. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableVoteEntryWrapper.class) +@JsonDeserialize(as = ImmutableVoteEntryWrapper.class) +@Beta +public interface VoteEntryWrapper { + + /** + * Construct a {@link VoteEntryWrapper} containing the specified + * {@link VoteEntry}. + * + * @param voteEntry A {@link VoteEntry}. + * + * @return A {@link VoteEntryWrapper}. + */ + static VoteEntryWrapper of(VoteEntry voteEntry) { + return ImmutableVoteEntryWrapper.builder() + .voteEntry(voteEntry) + .build(); + } + + /** + * A {@link VoteEntry}. + * + * @return A {@link VoteEntry}. + */ + @JsonProperty("VoteEntry") + VoteEntry voteEntry(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AccountSet.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AccountSet.java index ace5b6994..be1af6b7a 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AccountSet.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AccountSet.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. @@ -21,16 +21,19 @@ */ import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; import com.google.common.base.Preconditions; import com.google.common.primitives.UnsignedInteger; import org.immutables.value.Value; import org.xrpl.xrpl4j.model.flags.AccountSetTransactionFlags; -import org.xrpl.xrpl4j.model.flags.TransactionFlags; +import java.util.Arrays; +import java.util.Collections; import java.util.Optional; /** @@ -65,25 +68,189 @@ default AccountSetTransactionFlags flags() { /** * Unique identifier of a flag to disable for this account. * + *

If this field is empty, developers should check if {@link #clearFlagRawValue()} is also empty. If + * {@link #clearFlagRawValue()} is present, it means that the {@code ClearFlag} field of the transaction was not a + * valid {@link AccountSetFlag} but was still present in a validated transaction on ledger.

+ * *

Because the preferred way of setting account flags is with {@link AccountSetFlag}s, this field should * not be set in conjunction with the {@link AccountSet#flags()} field. * * @return An {@link Optional} of type {@link AccountSetFlag} representing the flag to disable on this account. */ - @JsonProperty("ClearFlag") + @JsonIgnore Optional clearFlag(); + /** + * A flag to disable for this account, as an {@link UnsignedInteger}. + * + *

Developers should prefer setting {@link #clearFlag()} and leaving this field empty when constructing + * a new {@link AccountSet}. This field is used to serialize and deserialize the {@code "ClearFlag"} field in JSON, + * as some {@link AccountSet} transactions on the XRPL set the "ClearFlag" field to a number that is not recognized as + * an asf flag by rippled. Without this field, xrpl4j would fail to deserialize those transactions, as + * {@link AccountSetFlag} does not support arbitrary integer values.

+ * + *

Additionally, using this field as the source of truth for JSON serialization/deserialization rather than + * {@link #clearFlag()} allows developers to recompute the hash of a transaction that was deserialized from a rippled + * RPC/WS result accurately. An alternative to this field would be to add an enum variant to {@link AccountSetFlag} + * for unknown values, but binary serializing an {@link AccountSet} that was constructed by deserializing JSON would + * result in a different binary blob than what exists on ledger.

+ * + * @return An {@link Optional} {@link UnsignedInteger}. + */ + @JsonProperty("ClearFlag") + Optional clearFlagRawValue(); + + /** + * Normalization method to try to get {@link #clearFlag()}and {@link #clearFlagRawValue()} to match. + * + *

If neither field is present, there is nothing to do.

+ *

If both fields are present, there is nothing to do, but we will check that {@link #clearFlag()}'s + * underlying value equals {@link #clearFlagRawValue()}.

+ *

If {@link #clearFlag()} is present but {@link #clearFlagRawValue()} is empty, we set + * {@link #clearFlagRawValue()} to the underlying value of {@link #clearFlag()}.

+ *

If {@link #clearFlag()} is empty and {@link #clearFlagRawValue()} is present, we will set + * {@link #clearFlag()} to the {@link AccountSetFlag} variant associated with {@link #clearFlagRawValue()}, or leave + * {@link #clearFlag()} empty if {@link #clearFlagRawValue()} does not map to an {@link AccountSetFlag}.

+ * + * @return A normalized {@link AccountSet}. + */ + @Value.Check + default AccountSet normalizeClearFlag() { + if (!clearFlag().isPresent() && !clearFlagRawValue().isPresent()) { + // If both are empty, nothing to do. + return this; + } else if (clearFlag().isPresent() && clearFlagRawValue().isPresent()) { + // Both will be present if: + // 1. A developer set them both manually (in the builder) + // 2. This normalize method has already been called. + + // We should still check that the clearFlagRawValue matches the inner value of AccountSetFlag. + Preconditions.checkState( + clearFlag().get().getValue() == clearFlagRawValue().get().longValue(), + String.format("clearFlag and clearFlagRawValue should be equivalent, but clearFlag's underlying " + + "value was %s and clearFlagRawValue was %s", + clearFlag().get().getValue(), + clearFlagRawValue().get().longValue() + ) + ); + return this; + } else if (clearFlag().isPresent() && !clearFlagRawValue().isPresent()) { + // This can only happen if the developer only set clearFlag(). In this case, we need to set clearFlagRawValue to + // match clearFlag. + return AccountSet.builder().from(this) + .clearFlagRawValue(UnsignedInteger.valueOf(clearFlag().get().getValue())) + .build(); + } else { // clearFlag not present and clearFlagRawValue is present + // This can happen if: + // 1. A developer sets clearFlagRawValue manually in the builder + // 2. JSON has ClearFlag and jackson sets clearFlagRawValue. + // This value will never be negative due to XRPL representing this kind of flag as an unsigned number, + // so no lower bound check is required. + if (clearFlagRawValue().get().longValue() <= AccountSetFlag.MAX_VALUE) { + // Set clearFlag to clearFlagRawValue if clearFlagRawValue matches a valid AccountSetFlag variant. + return AccountSet.builder().from(this) + .clearFlag(AccountSetFlag.forValue(clearFlagRawValue().get().intValue())) + .build(); + } else { + // Otherwise, leave clearFlag empty. + return this; + } + } + } + /** * Unique identifier of a flag to enable for this account. * - *

Because the preferred way of setting account flags is with {@link AccountSetFlag}s, this field should not be set - * in conjunction with the {@link AccountSet#flags()} field. + *

If this field is empty, developers should check if {@link #setFlagRawValue()} is also empty. If + * {@link #setFlagRawValue()} is present, it means that the {@code ClearFlag} field of the transaction was not a + * valid {@link AccountSetFlag} but was still present in a validated transaction on ledger.

+ * + *

Because the preferred way of setting account flags is with {@link AccountSetFlag}s, this field should not be + * set in conjunction with the {@link AccountSet#flags()} field. * * @return An {@link Optional} of type {@link AccountSetFlag} representing the flag to enable on this account. */ - @JsonProperty("SetFlag") + @JsonIgnore Optional setFlag(); + /** + * A flag to disable for this account, as an {@link UnsignedInteger}. + * + *

Developers should prefer setting {@link #setFlag()} and leaving this field empty when constructing + * a new {@link AccountSet}. This field is used to serialize and deserialize the {@code "ClearFlag"} field in JSON, + * as some {@link AccountSet} transactions on the XRPL set the "ClearFlag" field to a number that is not recognized as + * an asf flag by rippled. Without this field, xrpl4j would fail to deserialize those transactions, as + * {@link AccountSetFlag} does not support arbitrary integer values.

+ * + *

Additionally, using this field as the source of truth for JSON serialization/deserialization rather than + * {@link #setFlag()} allows developers to recompute the hash of a transaction that was deserialized from a rippled + * RPC/WS result accurately. An alternative to this field would be to add an enum variant to {@link AccountSetFlag} + * for unknown values, but binary serializing an {@link AccountSet} that was constructed by deserializing JSON would + * result in a different binary blob than what exists on ledger.

+ * + * @return An {@link Optional} {@link UnsignedInteger} + */ + @JsonProperty("SetFlag") + Optional setFlagRawValue(); + + /** + * Normalization method to try to get {@link #setFlag()}and {@link #setFlagRawValue()} to match. + * + *

If neither field is present, there is nothing to do.

+ *

If both fields are present, there is nothing to do, but we will check that {@link #setFlag()}'s + * underlying value equals {@link #setFlagRawValue()}.

+ *

If {@link #setFlag()} is present but {@link #setFlagRawValue()} is empty, we set + * {@link #setFlagRawValue()} to the underlying value of {@link #setFlag()}.

+ *

If {@link #setFlag()} is empty and {@link #setFlagRawValue()} is present, we will set + * {@link #setFlag()} to the {@link AccountSetFlag} variant associated with {@link #setFlagRawValue()}, or leave + * {@link #setFlag()} empty if {@link #setFlagRawValue()} does not map to an {@link AccountSetFlag}.

+ * + * @return A normalized {@link AccountSet}. + */ + @Value.Check + default AccountSet normalizeSetFlag() { + if (!setFlag().isPresent() && !setFlagRawValue().isPresent()) { + // If both are empty, nothing to do. + return this; + } else if (setFlag().isPresent() && setFlagRawValue().isPresent()) { + // Both will be present if: + // 1. A developer set them both manually (in the builder) + // 2. This normalize method has already been called. + + // We should still check that the setFlagRawValue matches the inner value of AccountSetFlag. + Preconditions.checkState( + setFlag().get().getValue() == setFlagRawValue().get().longValue(), + String.format("setFlag and setFlagRawValue should be equivalent, but setFlag's underlying " + + "value was %s and setFlagRawValue was %s", + setFlag().get().getValue(), + setFlagRawValue().get().longValue() + ) + ); + return this; + } else if (setFlag().isPresent() && !setFlagRawValue().isPresent()) { + // This can only happen if the developer only set setFlag(). In this case, we need to set setFlagRawValue to + // match setFlag. + return AccountSet.builder().from(this) + .setFlagRawValue(UnsignedInteger.valueOf(setFlag().get().getValue())) + .build(); + } else { // setFlag is empty and setFlagRawValue is present + // This can happen if: + // 1. A developer sets setFlagRawValue manually in the builder + // 2. JSON has ClearFlag and jackson sets setFlagRawValue. + // This value will never be negative due to XRPL representing this kind of flag as an unsigned number, + // so no lower bound check is required. + if (setFlagRawValue().get().longValue() <= AccountSetFlag.MAX_VALUE) { + // Set setFlag to setFlagRawValue if setFlagRawValue matches a valid AccountSetFlag variant. + return AccountSet.builder().from(this) + .setFlag(AccountSetFlag.forValue(setFlagRawValue().get().intValue())) + .build(); + } else { + // Otherwise, leave setFlag empty. + return this; + } + } + } + /** * The hex string of the lowercase ASCII of the domain for the account. For example, the domain example.com would be * represented as "6578616D706C652E636F6D". @@ -132,8 +299,8 @@ default AccountSetTransactionFlags flags() { Optional tickSize(); /** - * Sets an alternate account that is allowed to mint NFTokens on this - * account's behalf using NFTokenMint's `Issuer` field. + * Sets an alternate account that is allowed to mint NFTokens on this account's behalf using NFTokenMint's `Issuer` + * field. * * @return An {@link Optional} field of type {@link Address}. */ @@ -206,8 +373,8 @@ default void checkTickSize() { */ enum AccountSetFlag { /** - * This flag will do nothing but exists to accurately deserialize AccountSet transactions whose {@code SetFlag} - * or {@code ClearFlag} fields are zero. + * This flag will do nothing but exists to accurately deserialize AccountSet transactions whose {@code SetFlag} or + * {@code ClearFlag} fields are zero. */ NONE(0), /** @@ -276,7 +443,15 @@ enum AccountSetFlag { /** * Block incoming Trustlines. */ - DISALLOW_INCOMING_TRUSTLINE(15); + DISALLOW_INCOMING_TRUSTLINE(15), + /** + * Enable clawback on the account's trustlines. + * + *

This value will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject + * to change.

+ */ + @Beta + ALLOW_TRUSTLINE_CLAWBACK(16); final int value; @@ -284,6 +459,12 @@ enum AccountSetFlag { this.value = value; } + /** + * The maximum underlying value of AccountSetFlags. This is useful for the normalization methods of AccountSet + * so that adding a new AccountSetFlag does not require a change to those normalization functions. + */ + static final int MAX_VALUE = Collections.max(Arrays.asList(AccountSetFlag.values())).getValue(); + /** * To deserialize enums with integer values, you need to specify this factory method with the {@link JsonCreator} * annotation, otherwise Jackson treats the JSON integer value as an ordinal. @@ -291,6 +472,7 @@ enum AccountSetFlag { * @param value The int value of the flag. * * @return The {@link AccountSetFlag} for the given integer value. + * * @see "https://github.com/FasterXML/jackson-databind/issues/1850" */ @JsonCreator diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmBid.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmBid.java new file mode 100644 index 000000000..24872f827 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmBid.java @@ -0,0 +1,94 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; +import org.xrpl.xrpl4j.model.ledger.AuthAccountWrapper; +import org.xrpl.xrpl4j.model.ledger.Issue; + +import java.util.List; +import java.util.Optional; + +/** + * Object mapping for the AMMBid transaction. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmBid.class) +@JsonDeserialize(as = ImmutableAmmBid.class) +@Beta +public interface AmmBid extends Transaction { + + /** + * Construct a {@code AmmBid} builder. + * + * @return An {@link ImmutableAmmBid.Builder}. + */ + static ImmutableAmmBid.Builder builder() { + return ImmutableAmmBid.builder(); + } + + /** + * Set of {@link TransactionFlags}s for this {@link AmmBid}, which only allows the {@code tfFullyCanonicalSig} flag, + * which is deprecated. + * + *

The value of the flags cannot be set manually, but exists for JSON serialization/deserialization only and for + * proper signature computation in rippled. + * + * @return Always {@link TransactionFlags#EMPTY}. + */ + @JsonProperty("Flags") + @Value.Default + default TransactionFlags flags() { + return TransactionFlags.EMPTY; + } + + /** + * The definition for one of the assets in the AMM's pool. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset") + Issue asset(); + + /** + * The definition for the other asset in the AMM's pool. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset2") + Issue asset2(); + + /** + * Pay at least this amount for the slot. Setting this value higher makes it harder for others to outbid you. If + * omitted, pay the minimum necessary to win the bid. + * + * @return An optionally present {@link IssuedCurrencyAmount}. + */ + @JsonProperty("BidMin") + Optional bidMin(); + + /** + * Pay at most this amount for the slot. If the cost to win the bid is higher than this amount, the transaction fails. + * If omitted, pay as much as necessary to win the bid. + * + * @return An optionally present {@link IssuedCurrencyAmount}. + */ + @JsonProperty("BidMax") + Optional bidMax(); + + /** + * A list of up to 4 additional accounts that you allow to trade at the discounted fee. This cannot include the + * address of the transaction sender + * + * @return A {@link List} + */ + @JsonProperty("AuthAccounts") + List authAccounts(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmCreate.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmCreate.java new file mode 100644 index 000000000..dab53fa96 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmCreate.java @@ -0,0 +1,71 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.flags.Flags; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; + +/** + * Object mapping for the AMMCreate transaction. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmCreate.class) +@JsonDeserialize(as = ImmutableAmmCreate.class) +@Beta +public interface AmmCreate extends Transaction { + + /** + * Construct a {@code AmmCreate} builder. + * + * @return An {@link ImmutableAmmCreate.Builder}. + */ + static ImmutableAmmCreate.Builder builder() { + return ImmutableAmmCreate.builder(); + } + + /** + * Set of {@link TransactionFlags}s for this {@link AmmCreate}, which only allows the {@code tfFullyCanonicalSig} + * flag, which is deprecated. + * + *

The value of the flags cannot be set manually, but exists for JSON serialization/deserialization only and for + * proper signature computation in rippled. + * + * @return Always {@link TransactionFlags#EMPTY}. + */ + @JsonProperty("Flags") + @Value.Default + default TransactionFlags flags() { + return TransactionFlags.EMPTY; + } + + /** + * The first of the two assets to fund this AMM with. + * + * @return A {@link CurrencyAmount}. + */ + @JsonProperty("Amount") + CurrencyAmount amount(); + + /** + * The second of the two assets to fund this AMM with. + * + * @return A {@link CurrencyAmount}. + */ + @JsonProperty("Amount2") + CurrencyAmount amount2(); + + /** + * The fee to charge for trades against this AMM instance. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("TradingFee") + TradingFee tradingFee(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmDelete.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmDelete.java new file mode 100644 index 000000000..6af820fb3 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmDelete.java @@ -0,0 +1,61 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; +import org.xrpl.xrpl4j.model.ledger.Issue; + +/** + * Object mapping for the AMMDelete transaction. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Immutable +@JsonSerialize(as = ImmutableAmmDelete.class) +@JsonDeserialize(as = ImmutableAmmDelete.class) +@Beta +public interface AmmDelete extends Transaction { + + /** + * Construct a {@code AmmDelete} builder. + * + * @return An {@link ImmutableAmmDelete.Builder}. + */ + static ImmutableAmmDelete.Builder builder() { + return ImmutableAmmDelete.builder(); + } + + /** + * Set of {@link TransactionFlags}s for this {@link AmmDelete}, which only allows the {@code tfFullyCanonicalSig} + * flag, which is deprecated. + * + * @return Always {@link TransactionFlags#EMPTY}. + */ + @JsonProperty("Flags") + @Value.Default + default TransactionFlags flags() { + return TransactionFlags.EMPTY; + } + + /** + * The definition for one of the assets in the AMM's pool. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset") + Issue asset(); + + /** + * The definition for the other asset in the AMM's pool. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset2") + Issue asset2(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmDeposit.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmDeposit.java new file mode 100644 index 000000000..1336e27a6 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmDeposit.java @@ -0,0 +1,100 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.flags.AmmDepositFlags; +import org.xrpl.xrpl4j.model.ledger.Issue; + +import java.util.Optional; + +/** + * Object mapping for the AMMDeposit transaction. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmDeposit.class) +@JsonDeserialize(as = ImmutableAmmDeposit.class) +@Beta +public interface AmmDeposit extends Transaction { + + /** + * Construct a {@code AmmDeposit} builder. + * + * @return An {@link ImmutableAmmDeposit.Builder}. + */ + static ImmutableAmmDeposit.Builder builder() { + return ImmutableAmmDeposit.builder(); + } + + /** + * A {@link AmmDepositFlags} for this transaction. This field must be set manually. + * + * @return A {@link AmmDepositFlags} for this transaction. + */ + @JsonProperty("Flags") + AmmDepositFlags flags(); + + /** + * The definition for one of the assets in the AMM's pool. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset") + Issue asset(); + + /** + * The definition for the other asset in the AMM's pool. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset2") + Issue asset2(); + + /** + * The amount of one asset to deposit to the AMM. If present, this must match the type of one of the assets (tokens or + * XRP) in the AMM's pool. + * + * @return An optionally present {@link CurrencyAmount}. + */ + @JsonProperty("Amount") + Optional amount(); + + /** + * The amount of another asset to add to the AMM. If present, this must match the type of the other asset in the AMM's + * pool and cannot be the same asset as Amount. + * + * @return An optionally present {@link CurrencyAmount}. + */ + @JsonProperty("Amount2") + Optional amount2(); + + /** + * The maximum effective price, in the deposit asset, to pay for each LP Token received. + * + * @return An optionally present {@link CurrencyAmount}. + */ + @JsonProperty("EPrice") + Optional effectivePrice(); + + /** + * How many of the AMM's LP Tokens to buy. + * + * @return An optionally present {@link IssuedCurrencyAmount}. + */ + @JsonProperty("LPTokenOut") + Optional lpTokenOut(); + + /** + * An optional {@link TradingFee} to set on the AMM instance. This field is only honored if the AMM's LP token balance + * is zero, and can only be set if flags is {@link AmmDepositFlags#TWO_ASSET_IF_EMPTY}. + * + * @return An {@link Optional} {@link TradingFee}. + */ + @JsonProperty("TradingFee") + Optional tradingFee(); +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmVote.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmVote.java new file mode 100644 index 000000000..9742cfefb --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmVote.java @@ -0,0 +1,72 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; +import org.xrpl.xrpl4j.model.ledger.Issue; + +/** + * Object mapping for the AMMVote transaction. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableAmmVote.class) +@JsonDeserialize(as = ImmutableAmmVote.class) +@Beta +public interface AmmVote extends Transaction { + + /** + * Construct a {@code AmmVote} builder. + * + * @return An {@link ImmutableAmmVote.Builder}. + */ + static ImmutableAmmVote.Builder builder() { + return ImmutableAmmVote.builder(); + } + + /** + * Set of {@link TransactionFlags}s for this {@link AmmVote}, which only allows the {@code tfFullyCanonicalSig} flag, + * which is deprecated. + * + *

The value of the flags cannot be set manually, but exists for JSON serialization/deserialization only and for + * proper signature computation in rippled. + * + * @return Always {@link TransactionFlags#EMPTY}. + */ + @JsonProperty("Flags") + @Value.Default + default TransactionFlags flags() { + return TransactionFlags.EMPTY; + } + + /** + * The definition for one of the assets in the AMM's pool. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset") + Issue asset(); + + /** + * The definition for the other asset in the AMM's pool. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset2") + Issue asset2(); + + /** + * The proposed fee to vote for. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("TradingFee") + TradingFee tradingFee(); + + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmWithdraw.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmWithdraw.java new file mode 100644 index 000000000..3e0998fde --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmWithdraw.java @@ -0,0 +1,94 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.flags.AmmWithdrawFlags; +import org.xrpl.xrpl4j.model.ledger.Issue; + +import java.util.Optional; + +/** + * Object mapping for the AMMWithdraw transaction. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Immutable +@JsonSerialize(as = ImmutableAmmWithdraw.class) +@JsonDeserialize(as = ImmutableAmmWithdraw.class) +@Beta +public interface AmmWithdraw extends Transaction { + + /** + * Construct a {@code AmmWithdraw} builder. + * + * @return An {@link ImmutableAmmWithdraw.Builder}. + */ + static ImmutableAmmWithdraw.Builder builder() { + return ImmutableAmmWithdraw.builder(); + } + + /** + * A {@link AmmWithdrawFlags} for this transaction. + * + * @return A {@link AmmWithdrawFlags} for this transaction. + */ + @JsonProperty("Flags") + AmmWithdrawFlags flags(); + + /** + * The definition for one of the assets in the AMM's pool. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset") + Issue asset(); + + /** + * The definition for the other asset in the AMM's pool. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset2") + Issue asset2(); + + /** + * The amount of one asset to deposit to the AMM. If present, this must match the type of one of the assets (tokens or + * XRP) in the AMM's pool. + * + * @return An optionally present {@link CurrencyAmount}. + */ + @JsonProperty("Amount") + Optional amount(); + + /** + * The amount of another asset to add to the AMM. If present, this must match the type of the other asset in the AMM's + * pool and cannot be the same asset as Amount. + * + * @return An optionally present {@link CurrencyAmount}. + */ + @JsonProperty("Amount2") + Optional amount2(); + + /** + * The maximum effective price, in the deposit asset, to pay for each LP Token received. + * + * @return An optionally present {@link CurrencyAmount}. + */ + @JsonProperty("EPrice") + Optional effectivePrice(); + + /** + * How many of the AMM's LP Tokens to buy. + * + * @return An optionally present {@link IssuedCurrencyAmount}. + */ + @JsonProperty("LPTokensIn") + Optional lpTokensIn(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Clawback.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Clawback.java new file mode 100644 index 000000000..8c5c3bdcf --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Clawback.java @@ -0,0 +1,55 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; + +/** + * Clawback an issued currency that exists on a Trustline. + * + *

This class will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject + * to change.

+ */ +@Immutable +@JsonSerialize(as = ImmutableClawback.class) +@JsonDeserialize(as = ImmutableClawback.class) +@Beta +public interface Clawback extends Transaction { + + /** + * Construct a {@code Clawback} builder. + * + * @return An {@link ImmutableClawback.Builder}. + */ + static ImmutableClawback.Builder builder() { + return ImmutableClawback.builder(); + } + + /** + * Set of {@link TransactionFlags}s for this {@link Clawback}, which only allows the + * {@code tfFullyCanonicalSig} flag, which is deprecated. + * + * @return Always {@link TransactionFlags#EMPTY}. + */ + @JsonProperty("Flags") + @Value.Default + default TransactionFlags flags() { + return TransactionFlags.EMPTY; + } + + /** + * Indicates the amount being clawed back, as well as the counterparty from which the amount is being clawed back + * from. This amount must not exceed the holder's balance and must be greater than zero. The issuer in this amount + * must not be the same as the source account of this transaction. + * + * @return An {@link IssuedCurrencyAmount} indicating the amount to clawback. + */ + @JsonProperty("Amount") + IssuedCurrencyAmount amount(); + + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/EscrowFinish.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/EscrowFinish.java index 379d6a833..9aa5d8464 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/EscrowFinish.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/EscrowFinish.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. @@ -20,18 +20,28 @@ * =========================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.base.Preconditions; +import com.google.common.io.BaseEncoding; import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; import com.ripple.cryptoconditions.Condition; +import com.ripple.cryptoconditions.CryptoConditionReader; +import com.ripple.cryptoconditions.CryptoConditionWriter; import com.ripple.cryptoconditions.Fulfillment; +import com.ripple.cryptoconditions.der.DerEncodingException; import org.immutables.value.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.xrpl.xrpl4j.model.flags.TransactionFlags; import org.xrpl.xrpl4j.model.immutables.FluentCompareTo; +import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag; +import java.util.Arrays; +import java.util.Locale; import java.util.Objects; import java.util.Optional; @@ -43,6 +53,8 @@ @JsonDeserialize(as = ImmutableEscrowFinish.class) public interface EscrowFinish extends Transaction { + Logger logger = LoggerFactory.getLogger(EscrowFinish.class); + /** * Construct a builder for this class. * @@ -62,6 +74,7 @@ static ImmutableEscrowFinish.Builder builder() { * purposes. * * @return An {@link XrpCurrencyAmount} representing the computed fee. + * * @see "https://xrpl.org/escrowfinish.html" */ static XrpCurrencyAmount computeFee(final XrpCurrencyAmount currentLedgerFeeDrops, final Fulfillment fulfillment) { @@ -78,8 +91,8 @@ static XrpCurrencyAmount computeFee(final XrpCurrencyAmount currentLedgerFeeDrop } /** - * Set of {@link TransactionFlags}s for this {@link EscrowFinish}, which only allows the - * {@code tfFullyCanonicalSig} flag, which is deprecated. + * Set of {@link TransactionFlags}s for this {@link EscrowFinish}, which only allows the {@code tfFullyCanonicalSig} + * flag, which is deprecated. * *

The value of the flags cannot be set manually, but exists for JSON serialization/deserialization only and for * proper signature computation in rippled. @@ -111,34 +124,204 @@ default TransactionFlags flags() { /** * Hex value matching the previously-supplied PREIMAGE-SHA-256 crypto-condition of the held payment. * + *

If this field is empty, developers should check if {@link #conditionRawValue()} is also empty. If + * {@link #conditionRawValue()} is present, it means that the {@code "Condition"} field of the transaction was not a + * well-formed crypto-condition but was still present in a transaction on ledger.

+ * * @return An {@link Optional} of type {@link Condition} containing the escrow condition. */ - @JsonProperty("Condition") + @JsonIgnore Optional condition(); /** - * Hex value of the PREIMAGE-SHA-256 crypto-condition fulfillment matching the held payment's {@code condition}. + * The raw, hex-encoded PREIMAGE-SHA-256 crypto-condition of the escrow. + * + *

Developers should prefer setting {@link #condition()} and leaving this field empty when constructing a new + * {@link EscrowFinish}. This field is used to serialize and deserialize the {@code "Condition"} field in JSON, the + * XRPL will sometimes include an {@link EscrowFinish} in its ledger even if the crypto condition is malformed. + * Without this field, xrpl4j would fail to deserialize those transactions, as {@link #condition()} is typed as a + * {@link Condition}, which tries to decode the condition from DER.

+ * + *

Note that a similar field does not exist on {@link EscrowCreate}, + * {@link org.xrpl.xrpl4j.model.ledger.EscrowObject}, or + * {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} because {@link EscrowCreate}s with + * malformed conditions will never be included in a ledger by the XRPL. Because of this fact, an + * {@link org.xrpl.xrpl4j.model.ledger.EscrowObject} and + * {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} will also never contain a malformed + * crypto condition.

+ * + * @return An {@link Optional} {@link String} containing the hex-encoded PREIMAGE-SHA-256 condition. + */ + @JsonProperty("Condition") + Optional conditionRawValue(); + + /** + * Hex value of the PREIMAGE-SHA-256 crypto-condition fulfillment matching the held payment's {@link #condition()}. + * + *

If this field is empty, developers should check if {@link #fulfillmentRawValue()} is also empty. If + * {@link #fulfillmentRawValue()} is present, it means that the {@code "Fulfillment"} field of the transaction was not + * a well-formed crypto-condition fulfillment but was still present in a transaction on ledger.

* * @return An {@link Optional} of type {@link Fulfillment} containing the fulfillment for the escrow's condition. */ - @JsonProperty("Fulfillment") + @JsonIgnore Optional> fulfillment(); /** - * Validate fields. + * The raw, hex-encoded value of the PREIMAGE-SHA-256 crypto-condition fulfillment matching the held payment's + * {@link #condition()}. + * + *

Developers should prefer setting {@link #fulfillment()} and leaving this field empty when constructing a new + * {@link EscrowFinish}. This field is used to serialize and deserialize the {@code "Fulfillment"} field in JSON, the + * XRPL will sometimes include an {@link EscrowFinish} in its ledger even if the crypto fulfillment is malformed. + * Without this field, xrpl4j would fail to deserialize those transactions, as {@link #fulfillment()} is typed as a + * {@link Fulfillment}, which tries to decode the fulfillment from DER.

+ * + * @return An {@link Optional} {@link String} containing the hex-encoded PREIMAGE-SHA-256 fulfillment. + */ + @JsonProperty("Fulfillment") + Optional fulfillmentRawValue(); + + /** + * Normalization method to try to get {@link #condition()} and {@link #conditionRawValue()} to match. + * + *

If neither field is present, there is nothing to do.

+ *

If both fields are present, there is nothing to do, but we will check that {@link #condition()}'s + * underlying value equals {@link #conditionRawValue()}.

+ *

If {@link #condition()} is present but {@link #conditionRawValue()} is empty, we set + * {@link #conditionRawValue()} to the underlying value of {@link #condition()}.

+ *

If {@link #condition()} is empty and {@link #conditionRawValue()} is present, we will set + * {@link #condition()} to the {@link Condition} representing the raw condition value, or leave + * {@link #condition()} empty if {@link #conditionRawValue()} is a malformed {@link Condition}.

+ * + * @return A normalized {@link EscrowFinish}. */ @Value.Check - default void check() { - fulfillment().ifPresent(f -> { - UnsignedLong feeInDrops = fee().value(); - Preconditions.checkState(condition().isPresent(), - "If a fulfillment is specified, the corresponding condition must also be specified."); - Preconditions.checkState(FluentCompareTo.is(feeInDrops).greaterThanEqualTo(UnsignedLong.valueOf(330)), - "If a fulfillment is specified, the fee must be set to 330 or greater."); + default EscrowFinish normalizeCondition() { + try { + if (!condition().isPresent() && !conditionRawValue().isPresent()) { + // If both are empty, nothing to do. + return this; + } else if (condition().isPresent() && conditionRawValue().isPresent()) { + // Both will be present if: + // 1. A developer set them both manually (in the builder) + // 2. This method has already been called. + + // We should check that the condition()'s value matches the raw value. + Preconditions.checkState( + Arrays.equals(CryptoConditionWriter.writeCondition(condition().get()), + BaseEncoding.base16().decode(conditionRawValue().get())), + "condition and conditionRawValue should be equivalent if both are present." + ); + return this; + } else if (condition().isPresent() && !conditionRawValue().isPresent()) { + // This can only happen if the developer only set condition() because condition() will never be set + // after deserializing from JSON. In this case, we need to set conditionRawValue to match setFlag. + return EscrowFinish.builder().from(this) + .conditionRawValue(BaseEncoding.base16().encode(CryptoConditionWriter.writeCondition(condition().get()))) + .build(); + } else { // condition is empty and conditionRawValue is present + // This can happen if: + // 1. A developer sets conditionRawValue manually in the builder + // 2. JSON has Condition and Jackson sets conditionRawValue + + // In this case, we should try to read conditionRawValue to a Condition. If that fails, condition() + // will remain empty, otherwise we will set condition(). + try { + Condition condition = CryptoConditionReader.readCondition( + BaseEncoding.base16().decode(conditionRawValue().get().toUpperCase(Locale.US)) + ); + return EscrowFinish.builder().from(this) + .condition(condition) + .build(); + } catch (DerEncodingException | IllegalArgumentException e) { + logger.warn( + "EscrowFinish Condition was malformed. conditionRawValue() will contain the condition value, but " + + "condition() will be empty: {}", + e.getMessage(), + e + ); + return this; + } } - ); - condition().ifPresent($ -> Preconditions.checkState(fulfillment().isPresent(), - "If a condition is specified, the corresponding fulfillment must also be specified.")); + + } catch (DerEncodingException e) { + // This should never happen. CryptoconditionWriter.writeCondition errantly declares that it can throw + // a DerEncodingException, but nowhere in its implementation does it throw. + throw new RuntimeException(e); + } + } + + /** + * Normalization method to try to get {@link #fulfillment()} and {@link #fulfillmentRawValue()} to match. + * + *

If neither field is present, there is nothing to do.

+ *

If both fields are present, there is nothing to do, but we will check that {@link #fulfillment()}'s + * underlying value equals {@link #fulfillmentRawValue()}.

+ *

If {@link #fulfillment()} is present but {@link #fulfillmentRawValue()} is empty, we set + * {@link #fulfillmentRawValue()} to the underlying value of {@link #fulfillment()}.

+ *

If {@link #fulfillment()} is empty and {@link #fulfillmentRawValue()} is present, we will set + * {@link #fulfillment()} to the {@link Fulfillment} representing the raw fulfillment value, or leave + * {@link #fulfillment()} empty if {@link #fulfillmentRawValue()} is a malformed {@link Fulfillment}.

+ * + * @return A normalized {@link EscrowFinish}. + */ + @Value.Check + default EscrowFinish normalizeFulfillment() { + try { + if (!fulfillment().isPresent() && !fulfillmentRawValue().isPresent()) { + // If both are empty, nothing to do. + return this; + } else if (fulfillment().isPresent() && fulfillmentRawValue().isPresent()) { + // Both will be present if: + // 1. A developer set them both manually (in the builder) + // 2. This method has already been called. + + // We should check that the fulfillment()'s value matches the raw value. + Preconditions.checkState( + Arrays.equals(CryptoConditionWriter.writeFulfillment(fulfillment().get()), + BaseEncoding.base16().decode(fulfillmentRawValue().get())), + "fulfillment and fulfillmentRawValue should be equivalent if both are present." + ); + return this; + } else if (fulfillment().isPresent() && !fulfillmentRawValue().isPresent()) { + // This can only happen if the developer only set fulfillment() because fulfillment() will never be set + // after deserializing from JSON. In this case, we need to set fulfillmentRawValue to match setFlag. + return EscrowFinish.builder().from(this) + .fulfillmentRawValue( + BaseEncoding.base16().encode(CryptoConditionWriter.writeFulfillment(fulfillment().get())) + ) + .build(); + } else { // fulfillment is empty and fulfillmentRawValue is present + // This can happen if: + // 1. A developer sets fulfillmentRawValue manually in the builder + // 2. JSON has Condition and Jackson sets fulfillmentRawValue + + // In this case, we should try to read fulfillmentRawValue to a Condition. If that fails, fulfillment() + // will remain empty, otherwise we will set fulfillment(). + try { + Fulfillment fulfillment = CryptoConditionReader.readFulfillment( + BaseEncoding.base16().decode(fulfillmentRawValue().get().toUpperCase(Locale.US)) + ); + return EscrowFinish.builder().from(this) + .fulfillment(fulfillment) + .build(); + } catch (DerEncodingException | IllegalArgumentException e) { + logger.warn( + "EscrowFinish Fulfillment was malformed. fulfillmentRawValue() will contain the fulfillment value, " + + "but fulfillment() will be empty: {}", + e.getMessage(), + e + ); + return this; + } + } + + } catch (DerEncodingException e) { + // This should never happen. CryptoconditionWriter.writeCondition errantly declares that it can throw + // a DerEncodingException, but nowhere in its implementation does it throw. + throw new RuntimeException(e); + } } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/SignerListSet.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/SignerListSet.java index e6bb6eac3..9128e2f7f 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/SignerListSet.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/SignerListSet.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. @@ -31,8 +31,8 @@ import java.util.List; /** - * The SignerListSet transaction creates, replaces, or removes a list of signers that can be used - * to multi-sign a {@link Transaction}. + * The SignerListSet transaction creates, replaces, or removes a list of signers that can be used to multi-sign a + * {@link Transaction}. */ @Value.Immutable @JsonSerialize(as = ImmutableSignerListSet.class) @@ -49,8 +49,8 @@ static ImmutableSignerListSet.Builder builder() { } /** - * Set of {@link TransactionFlags}s for this {@link SignerListSet}, which only allows the - * {@code tfFullyCanonicalSig} flag, which is deprecated. + * Set of {@link TransactionFlags}s for this {@link SignerListSet}, which only allows the {@code tfFullyCanonicalSig} + * flag, which is deprecated. * *

The value of the flags cannot be set manually, but exists for JSON serialization/deserialization only and for * proper signature computation in rippled. @@ -64,8 +64,8 @@ default TransactionFlags flags() { } /** - * A target number for the signer weights. A multi-signature from this list is valid only if the sum weights of - * the signatures provided is greater than or equal to this value. To delete a signer list, use the value 0. + * A target number for the signer weights. A multi-signature from this list is valid only if the sum weights of the + * signatures provided is greater than or equal to this value. To delete a signer list, use the value 0. * * @return An {@link UnsignedInteger} representing the singer quorum. */ @@ -73,10 +73,10 @@ default TransactionFlags flags() { UnsignedInteger signerQuorum(); /** - * (Omitted when deleting) Array of {@link org.xrpl.xrpl4j.model.ledger.SignerEntry} objects, indicating the - * addresses and weights of signers in this list. This signer list must have at least 1 member and no more - * than 8 members. No {@link Address} may appear more than once in the list, nor may the {@link #account()} - * submitting the transaction appear in the list. + * (Omitted when deleting) Array of {@link org.xrpl.xrpl4j.model.ledger.SignerEntry} objects, indicating the addresses + * and weights of signers in this list. This signer list must have at least 1 member and no more than 8 members. No + * {@link Address} may appear more than once in the list, nor may the {@link #account()} submitting the transaction + * appear in the list. * * @return A {@link List} of {@link SignerEntryWrapper}s. */ diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java index 3eca5a090..1d756cf96 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java @@ -71,6 +71,13 @@ public interface Transaction { .put(ImmutableTrustSet.class, TransactionType.TRUST_SET) .put(ImmutableTicketCreate.class, TransactionType.TICKET_CREATE) .put(ImmutableUnlModify.class, TransactionType.UNL_MODIFY) + .put(ImmutableAmmBid.class, TransactionType.AMM_BID) + .put(ImmutableAmmCreate.class, TransactionType.AMM_CREATE) + .put(ImmutableAmmDeposit.class, TransactionType.AMM_DEPOSIT) + .put(ImmutableAmmVote.class, TransactionType.AMM_VOTE) + .put(ImmutableAmmWithdraw.class, TransactionType.AMM_WITHDRAW) + .put(ImmutableAmmDelete.class, TransactionType.AMM_DELETE) + .put(ImmutableClawback.class, TransactionType.CLAWBACK) .build(); /** diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java index 1d01502ae..87a293c00 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.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. @@ -21,6 +21,7 @@ */ import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.annotations.Beta; /** * Enumeration of the types of Transactions on the XRP Ledger. @@ -160,7 +161,70 @@ public enum TransactionType { /** * The {@link TransactionType} for the {@link UnlModify} transaction. */ - UNL_MODIFY("UNLModify"); + UNL_MODIFY("UNLModify"), + + /** + * The {@link TransactionType} for the {@link Clawback} transaction. + * + *

This constant will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject + * to change.

+ */ + @Beta + CLAWBACK("Clawback"), + + /** + * The {@link TransactionType} for the {@link AmmBid} transaction. + * + *

This constant will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ + @Beta + AMM_BID("AMMBid"), + + /** + * The {@link TransactionType} for the {@link AmmCreate} transaction. + * + *

This constant will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ + @Beta + AMM_CREATE("AMMCreate"), + + /** + * The {@link TransactionType} for the {@link AmmDeposit} transaction. + * + *

This constant will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ + @Beta + AMM_DEPOSIT("AMMDeposit"), + + /** + * The {@link TransactionType} for the {@link AmmVote} transaction. + * + *

This constant will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ + @Beta + AMM_VOTE("AMMVote"), + + /** + * The {@link TransactionType} for the {@link AmmWithdraw} transaction. + * + *

This constant will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ + @Beta + AMM_WITHDRAW("AMMWithdraw"), + + /** + * The {@link TransactionType} for the {@link AmmDelete} transaction. + * + *

This constant will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ + @Beta + AMM_DELETE("AMMDelete"); private final String value; diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Wrappers.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Wrappers.java index 3e887b29a..d746b27f7 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Wrappers.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Wrappers.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonRawValue; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; import com.google.common.base.Preconditions; import com.google.common.io.BaseEncoding; import com.google.common.primitives.UnsignedInteger; @@ -42,8 +43,12 @@ import org.xrpl.xrpl4j.model.jackson.modules.NfTokenIdDeserializer; import org.xrpl.xrpl4j.model.jackson.modules.NfTokenIdSerializer; import org.xrpl.xrpl4j.model.jackson.modules.NfTokenUriSerializer; +import org.xrpl.xrpl4j.model.jackson.modules.TradingFeeDeserializer; +import org.xrpl.xrpl4j.model.jackson.modules.TradingFeeSerializer; import org.xrpl.xrpl4j.model.jackson.modules.TransferFeeDeserializer; import org.xrpl.xrpl4j.model.jackson.modules.TransferFeeSerializer; +import org.xrpl.xrpl4j.model.jackson.modules.VoteWeightDeserializer; +import org.xrpl.xrpl4j.model.jackson.modules.VoteWeightSerializer; import org.xrpl.xrpl4j.model.jackson.modules.XrpCurrencyAmountDeserializer; import org.xrpl.xrpl4j.model.jackson.modules.XrpCurrencyAmountSerializer; @@ -339,8 +344,8 @@ public boolean equals(Object obj) { * A wrapped {@link com.google.common.primitives.UnsignedInteger} containing the TransferFee. * *

Valid values for this field are between 0 and 50000 inclusive, allowing transfer rates of between 0.00% and - * 50.00% in increments of 0.001. If this field is provided in a {@link NfTokenMint} transaction, the transaction - * MUST have the {@code tfTransferable} flag enabled. + * 50.00% in increments of 0.001. If this field is provided in a {@link NfTokenMint} transaction, the transaction MUST + * have the {@code tfTransferable} flag enabled. */ @Value.Immutable @Wrapped @@ -412,4 +417,77 @@ public static NetworkId of(long networkId) { return NetworkId.of(UnsignedInteger.valueOf(networkId)); } } + + /** + * A wrapped {@link com.google.common.primitives.UnsignedInteger} containing the TransferFee. + * + *

This class will be marked {@link com.google.common.annotations.Beta} until the AMM amendment is enabled on + * mainnet. Its API is subject to change.

+ */ + @Value.Immutable + @Wrapped + @JsonSerialize(as = TradingFee.class, using = TradingFeeSerializer.class) + @JsonDeserialize(as = TradingFee.class, using = TradingFeeDeserializer.class) + @Beta + abstract static class _TradingFee extends Wrapper implements Serializable { + + @Override + public String toString() { + return this.value().toString(); + } + + /** + * Construct {@link TradingFee} as a percentage value. + * + * @param percent The trading fee, as a {@link BigDecimal}. + * + * @return A {@link TradingFee}. + */ + public static TradingFee ofPercent(BigDecimal percent) { + Preconditions.checkArgument( + Math.max(0, percent.stripTrailingZeros().scale()) <= 3, + "Percent value should have a maximum of 3 decimal places." + ); + return TradingFee.of(UnsignedInteger.valueOf(percent.scaleByPowerOfTen(3).toBigIntegerExact())); + } + + /** + * Get the {@link TradingFee} as a {@link BigDecimal}. + * + * @return A {@link BigDecimal}. + */ + public BigDecimal bigDecimalValue() { + return BigDecimal.valueOf(value().longValue(), 3); + } + + } + + /** + * A wrapped {@link com.google.common.primitives.UnsignedInteger} containing the VoteWeight. + * + *

This class will be marked {@link com.google.common.annotations.Beta} until the AMM amendment is enabled on + * mainnet. Its API is subject to change.

+ */ + @Value.Immutable + @Wrapped + @JsonSerialize(as = VoteWeight.class, using = VoteWeightSerializer.class) + @JsonDeserialize(as = VoteWeight.class, using = VoteWeightDeserializer.class) + @Beta + abstract static class _VoteWeight extends Wrapper implements Serializable { + + @Override + public String toString() { + return this.value().toString(); + } + + /** + * Get the {@link VoteWeight} as a {@link BigDecimal}. + * + * @return A {@link BigDecimal}. + */ + public BigDecimal bigDecimalValue() { + return BigDecimal.valueOf(value().longValue(), 3); + } + + } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/AffectedNode.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/AffectedNode.java index 16de8dc46..43a12427a 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/AffectedNode.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/AffectedNode.java @@ -22,6 +22,7 @@ public interface AffectedNode { * @param createdNodeConsumer A {@link Consumer} that is called if this instance is of type {@link CreatedNode}. * @param modifiedNodeConsumer A {@link Consumer} that is called if this instance is of type {@link ModifiedNode}. * @param deletedNodeConsumer A {@link Consumer} that is called if this instance is of type {@link DeletedNode}. + * @param An instance that extends {@link MetaLedgerObject}. */ default void handle( final Consumer> createdNodeConsumer, @@ -50,7 +51,9 @@ default void handle( * @param modifiedNodeMapper A {@link Function} that is called if this instance is of type {@link ModifiedNode}. * @param deletedNodeMapper A {@link Function} that is called if this instance is of type {@link DeletedNode}. * @param The type of object to return after mapping. - * @return A {@link R} that is constructed by the appropriate mapper function. + * @param An instance that extends {@link MetaLedgerObject}. + * + * @return An {@link R} that is constructed by the appropriate mapper function. */ default R map( final Function, R> createdNodeMapper, diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAmmObject.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAmmObject.java new file mode 100644 index 000000000..0274920f5 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAmmObject.java @@ -0,0 +1,116 @@ +package org.xrpl.xrpl4j.model.transactions.metadata; + +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.annotations.Beta; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.flags.Flags; +import org.xrpl.xrpl4j.model.ledger.AuctionSlot; +import org.xrpl.xrpl4j.model.ledger.Issue; +import org.xrpl.xrpl4j.model.ledger.VoteEntryWrapper; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TradingFee; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Represents an AMM ledger object, which describes a single Automated Market Maker instance. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Immutable +@JsonSerialize(as = ImmutableMetaAmmObject.class) +@JsonDeserialize(as = ImmutableMetaAmmObject.class) +@Beta +public interface MetaAmmObject extends MetaLedgerObject { + + /** + * A bit-map of boolean flags. No flags are defined for {@link MetaAmmObject}, so this value is always 0. + * + * @return Always {@link Flags#UNSET}. + */ + @JsonProperty("Flags") + @Value.Derived + default Flags flags() { + return Flags.UNSET; + } + + /** + * The definition for one of the two assets this AMM holds. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset") + Optional asset(); + + /** + * The definition for the other asset this AMM holds. + * + * @return An {@link Issue}. + */ + @JsonProperty("Asset2") + Optional asset2(); + + /** + * The address of the special account that holds this AMM's assets. + * + * @return An {@link Address}. + */ + @JsonProperty("Account") + Optional
account(); + + /** + * Details of the current owner of the auction slot. + * + * @return A {@link MetaAuctionSlot}. + */ + @JsonProperty("AuctionSlot") + Optional auctionSlot(); + + /** + * The total outstanding balance of liquidity provider tokens from this AMM instance. The holders of these tokens can + * vote on the AMM's trading fee in proportion to their holdings, or redeem the tokens for a share of the AMM's assets + * which grows with the trading fees collected. + * + * @return An {@link IssuedCurrencyAmount}. + */ + @JsonProperty("LPTokenBalance") + Optional lpTokenBalance(); + + /** + * The percentage fee to be charged for trades against this AMM instance, in units of 1/10,000. The maximum value is + * 1000, for a 1% fee. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("TradingFee") + Optional tradingFee(); + + /** + * A list of vote objects, representing votes on the pool's trading fee. + * + * @return A {@link List} of {@link MetaVoteEntryWrapper}s. + */ + @JsonProperty("VoteSlots") + List voteSlots(); + + /** + * Unwraps the {@link MetaVoteEntryWrapper}s in {@link #voteSlots()} for easier access to {@link MetaVoteEntry}s. + * + * @return A {@link List} of {@link MetaVoteEntry}. + */ + @JsonIgnore + @Value.Derived + default List voteSlotsUnwrapped() { + return voteSlots().stream() + .map(MetaVoteEntryWrapper::voteEntry) + .collect(Collectors.toList()); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAuctionSlot.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAuctionSlot.java new file mode 100644 index 000000000..f61c74c66 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAuctionSlot.java @@ -0,0 +1,89 @@ +package org.xrpl.xrpl4j.model.transactions.metadata; + +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.annotations.Beta; +import com.google.common.primitives.UnsignedInteger; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.ledger.AmmObject; +import org.xrpl.xrpl4j.model.ledger.ImmutableAuctionSlot; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TradingFee; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Represents an AuctionSlot object in an {@link AmmObject}, containing details of the current owner of the auction + * slot. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Immutable +@JsonSerialize(as = ImmutableMetaAuctionSlot.class) +@JsonDeserialize(as = ImmutableMetaAuctionSlot.class) +@Beta +public interface MetaAuctionSlot { + + /** + * The current owner of this auction slot. + * + * @return An {@link Address}. + */ + @JsonProperty("Account") + Optional
account(); + + /** + * A list of at most 4 additional accounts that are authorized to trade at the discounted fee for this AMM instance. + * + * @return A {@link List} of {@link MetaAuthAccountWrapper}s. + */ + @JsonProperty("AuthAccounts") + List authAccounts(); + + /** + * Extracts all the addresses found in the {@link MetaAuthAccount}s found in {@link #authAccounts()}. + * + * @return A {@link List} of {@link Address}. + */ + @JsonIgnore + @Value.Derived + default List
authAccountsAddresses() { + return authAccounts().stream() + .map(MetaAuthAccountWrapper::authAccount) + .map(MetaAuthAccount::account) + .collect(Collectors.toList()); + } + + /** + * The trading fee to be charged to the auction owner. By default this is 0, meaning that the auction owner can trade + * at no fee instead of the standard fee for this AMM. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("DiscountedFee") + Optional discountedFee(); + + /** + * The amount the auction owner paid to win this slot, in LP Tokens. + * + * @return An {@link IssuedCurrencyAmount}. + */ + @JsonProperty("Price") + Optional price(); + + /** + * The time when this slot expires, in seconds since the Ripple Epoch. + * + * @return An {@link UnsignedInteger} + */ + @JsonProperty("Expiration") + Optional expiration(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAuthAccount.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAuthAccount.java new file mode 100644 index 000000000..5f997cad4 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAuthAccount.java @@ -0,0 +1,32 @@ +package org.xrpl.xrpl4j.model.transactions.metadata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.ledger.ImmutableAuthAccount; +import org.xrpl.xrpl4j.model.transactions.Address; + +/** + * An account that is authorized to trade at the discounted fee for an AMM instance. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Immutable +@JsonSerialize(as = ImmutableMetaAuthAccount.class) +@JsonDeserialize(as = ImmutableMetaAuthAccount.class) +@Beta +public interface MetaAuthAccount { + + /** + * The address of the account. + * + * @return An {@link Address}. + */ + @JsonProperty("Account") + Address account(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAuthAccountWrapper.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAuthAccountWrapper.java new file mode 100644 index 000000000..fe1c190f6 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaAuthAccountWrapper.java @@ -0,0 +1,31 @@ +package org.xrpl.xrpl4j.model.transactions.metadata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.ledger.ImmutableAuthAccountWrapper; + +/** + * A wrapper around {@link MetaAuthAccount}s. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Immutable +@JsonSerialize(as = ImmutableMetaAuthAccountWrapper.class) +@JsonDeserialize(as = ImmutableMetaAuthAccountWrapper.class) +@Beta +public interface MetaAuthAccountWrapper { + + /** + * An {@link MetaAuthAccount}. + * + * @return An {@link MetaAuthAccount}. + */ + @JsonProperty("AuthAccount") + MetaAuthAccount authAccount(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryType.java index b060db762..f1f07f9fd 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryType.java @@ -25,6 +25,7 @@ public interface MetaLedgerEntryType { MetaLedgerEntryType SIGNER_LIST = MetaLedgerEntryType.of("SignerList"); MetaLedgerEntryType TICKET = MetaLedgerEntryType.of("Ticket"); MetaLedgerEntryType NFTOKEN_PAGE = MetaLedgerEntryType.of("NFTokenPage"); + MetaLedgerEntryType AMM = MetaLedgerEntryType.of("AMM"); /** * Construct a new {@link MetaLedgerEntryType} from a {@link String}. diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVoteEntry.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVoteEntry.java new file mode 100644 index 000000000..4e529e71d --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVoteEntry.java @@ -0,0 +1,52 @@ +package org.xrpl.xrpl4j.model.transactions.metadata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.ledger.ImmutableVoteEntry; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.TradingFee; +import org.xrpl.xrpl4j.model.transactions.VoteWeight; + +import java.util.Optional; + +/** + * Describes a vote for the trading fee on an AMM by an LP. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Immutable +@JsonSerialize(as = ImmutableMetaVoteEntry.class) +@JsonDeserialize(as = ImmutableMetaVoteEntry.class) +@Beta +public interface MetaVoteEntry { + + /** + * The address of the LP who voted. + * + * @return An {@link Address}. + */ + @JsonProperty("Account") + Optional
account(); + + /** + * The trading fee that the LP voted for. + * + * @return A {@link TradingFee}. + */ + @JsonProperty("TradingFee") + Optional tradingFee(); + + /** + * The weight of the LP's vote. + * + * @return The {@link VoteWeight}. + */ + @JsonProperty("VoteWeight") + Optional voteWeight(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVoteEntryWrapper.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVoteEntryWrapper.java new file mode 100644 index 000000000..b92551f0c --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVoteEntryWrapper.java @@ -0,0 +1,31 @@ +package org.xrpl.xrpl4j.model.transactions.metadata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.ledger.ImmutableVoteEntryWrapper; + +/** + * A wrapper around a {@link MetaVoteEntry}. + * + *

This class will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject to + * change.

+ */ +@Immutable +@JsonSerialize(as = ImmutableMetaVoteEntryWrapper.class) +@JsonDeserialize(as = ImmutableMetaVoteEntryWrapper.class) +@Beta +public interface MetaVoteEntryWrapper { + + /** + * A {@link MetaVoteEntry}. + * + * @return A {@link MetaVoteEntry}. + */ + @JsonProperty("VoteEntry") + MetaVoteEntry voteEntry(); + +} diff --git a/xrpl4j-core/src/main/resources/definitions.json b/xrpl4j-core/src/main/resources/definitions.json index 1911bd47a..b6b48f440 100644 --- a/xrpl4j-core/src/main/resources/definitions.json +++ b/xrpl4j-core/src/main/resources/definitions.json @@ -22,6 +22,7 @@ "UInt384": 22, "UInt512": 23, "Issue": 24, + "XChainBridge": 25, "Transaction": 10001, "LedgerEntry": 10002, "Validation": 10003, @@ -35,8 +36,11 @@ "Ticket": 84, "SignerList": 83, "Offer": 111, + "Bridge": 105, "LedgerHashes": 104, "Amendments": 102, + "XChainOwnedClaimID": 113, + "XChainOwnedCreateAccountClaimID": 116, "FeeSettings": 115, "Escrow": 117, "PayChannel": 120, @@ -233,6 +237,16 @@ "type": "UInt8" } ], + [ + "WasLockingChainSend", + { + "nth": 19, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt8" + } + ], [ "LedgerEntryType", { @@ -983,6 +997,36 @@ "type": "UInt64" } ], + [ + "XChainClaimID", + { + "nth": 20, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "XChainAccountCreateCount", + { + "nth": 21, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "XChainAccountClaimCount", + { + "nth": 22, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], [ "EmailHash", { @@ -1583,6 +1627,26 @@ "type": "Amount" } ], + [ + "SignatureReward", + { + "nth": 29, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "MinAccountCreateAmount", + { + "nth": 30, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], [ "LPTokenBalance", { @@ -1933,6 +1997,66 @@ "type": "AccountID" } ], + [ + "OtherChainSource", + { + "nth": 18, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "OtherChainDestination", + { + "nth": 19, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "AttestationSignerAccount", + { + "nth": 20, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "AttestationRewardAccount", + { + "nth": 21, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "LockingChainDoor", + { + "nth": 22, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "IssuingChainDoor", + { + "nth": 23, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], [ "Indexes", { @@ -1983,6 +2107,26 @@ "type": "PathSet" } ], + [ + "LockingChainIssue", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Issue" + } + ], + [ + "IssuingChainIssue", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Issue" + } + ], [ "Asset", { @@ -2003,6 +2147,16 @@ "type": "Issue" } ], + [ + "XChainBridge", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "XChainBridge" + } + ], [ "TransactionMetaData", { @@ -2243,6 +2397,46 @@ "type": "STObject" } ], + [ + "XChainClaimProofSig", + { + "nth": 28, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "XChainCreateAccountProofSig", + { + "nth": 29, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "XChainClaimAttestationCollectionElement", + { + "nth": 30, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "XChainCreateAccountAttestationCollectionElement", + { + "nth": 31, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], [ "Signers", { @@ -2393,6 +2587,26 @@ "type": "STArray" } ], + [ + "XChainClaimAttestations", + { + "nth": 21, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "XChainCreateAccountAttestations", + { + "nth": 22, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], [ "AuthAccounts", { @@ -2461,6 +2675,12 @@ "temSEQ_AND_TICKET": -263, "temBAD_NFTOKEN_TRANSFER_FEE": -262, "temBAD_AMM_TOKENS": -261, + "temXCHAIN_EQUAL_DOOR_ACCOUNTS": -260, + "temXCHAIN_BAD_PROOF": -259, + "temXCHAIN_BRIDGE_BAD_ISSUES": -258, + "temXCHAIN_BRIDGE_NONDOOR_OWNER": -257, + "temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT": -256, + "temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT": -255, "tefFAILURE": -199, "tefALREADY": -198, @@ -2497,6 +2717,7 @@ "terQUEUED": -89, "terPRE_TICKET": -88, "terNO_AMM": -87, + "terSUBMITTED": -86, "tesSUCCESS": 0, @@ -2538,6 +2759,7 @@ "tecKILLED": 150, "tecHAS_OBLIGATIONS": 151, "tecTOO_SOON": 152, + "tecHOOK_ERROR": 153, "tecMAX_SEQUENCE_REACHED": 154, "tecNO_SUITABLE_NFTOKEN_PAGE": 155, "tecNFTOKEN_BUY_SELL_MISMATCH": 156, @@ -2549,7 +2771,28 @@ "tecUNFUNDED_AMM": 162, "tecAMM_BALANCE": 163, "tecAMM_FAILED": 164, - "tecAMM_INVALID_TOKENS": 165 + "tecAMM_INVALID_TOKENS": 165, + "tecAMM_EMPTY": 166, + "tecAMM_NOT_EMPTY": 167, + "tecAMM_ACCOUNT": 168, + "tecINCOMPLETE": 169, + "tecXCHAIN_BAD_TRANSFER_ISSUE": 170, + "tecXCHAIN_NO_CLAIM_ID": 171, + "tecXCHAIN_BAD_CLAIM_ID": 172, + "tecXCHAIN_CLAIM_NO_QUORUM": 173, + "tecXCHAIN_PROOF_UNKNOWN_KEY": 174, + "tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE": 175, + "tecXCHAIN_WRONG_CHAIN": 176, + "tecXCHAIN_REWARD_MISMATCH": 177, + "tecXCHAIN_NO_SIGNERS_LIST": 178, + "tecXCHAIN_SENDING_ACCOUNT_MISMATCH": 179, + "tecXCHAIN_INSUFF_CREATE_AMOUNT": 180, + "tecXCHAIN_ACCOUNT_CREATE_PAST": 181, + "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182, + "tecXCHAIN_PAYMENT_FAILED": 183, + "tecXCHAIN_SELF_COMMIT": 184, + "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 185, + "tecXCHAIN_CREATE_ACCOUNT_DISABLED": 186 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2587,6 +2830,15 @@ "AMMWithdraw": 37, "AMMVote": 38, "AMMBid": 39, + "AMMDelete": 40, + "XChainCreateClaimID": 41, + "XChainCommit": 42, + "XChainClaim": 43, + "XChainAccountCreateCommit": 44, + "XChainAddClaimAttestation": 45, + "XChainAddAccountCreateAttestation": 46, + "XChainModifyBridge": 47, + "XChainCreateBridge": 48, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/PrivateKeyCodecTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/PrivateKeyCodecTest.java new file mode 100644 index 000000000..ef9e97f1a --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/PrivateKeyCodecTest.java @@ -0,0 +1,90 @@ +package org.xrpl.xrpl4j.codec.addresses; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.crypto.TestConstants; +import org.xrpl.xrpl4j.crypto.keys.Passphrase; +import org.xrpl.xrpl4j.crypto.keys.Seed; + +/** + * Unit tests for {@link PrivateKeyCodec}. + */ +class PrivateKeyCodecTest extends AbstractCodecTest { + + private PrivateKeyCodec privateKeyCodec; + + @BeforeEach + void setUp() { + privateKeyCodec = PrivateKeyCodec.getInstance(); + } + + @Test + public void encodeDecodeEdNodePrivate() { + testEncodeDecode( + prefixedNodePrivateKey -> privateKeyCodec.encodeNodePrivateKey(prefixedNodePrivateKey), + prefixedNodePrivateKey -> privateKeyCodec.decodeNodePrivateKey(prefixedNodePrivateKey), + TestConstants.getEdPrivateKey().naturalBytes(), + "paZHnTCvwm4GsxZ7qiA2nUBKE2DLnCoDWYqyocVZfVEZx3kvA4u" + ); + } + + /** + * These values come from the rippled codebase in Seed_test.cpp. + */ + @Test + public void encodeDecodeNodePrivateFromRippled() { + Seed seed = Seed.ed25519SeedFromPassphrase(Passphrase.of("masterpassphrase")); + + testEncodeDecode( + prefixedNodePrivateKey -> privateKeyCodec.encodeNodePrivateKey(prefixedNodePrivateKey), + prefixedNodePrivateKey -> privateKeyCodec.decodeNodePrivateKey(prefixedNodePrivateKey), + seed.deriveKeyPair().privateKey().naturalBytes(), + "paKv46LztLqK3GaKz1rG2nQGN6M4JLyRtxFBYFTw4wAVHtGys36" + ); + } + + @Test + public void encodeDecodeEdAccountPrivateKey() { + testEncodeDecode( + prefixedAccountPrivateKey -> privateKeyCodec.encodeAccountPrivateKey(prefixedAccountPrivateKey), + prefixedAccountPrivateKey -> privateKeyCodec.decodeAccountPrivateKey(prefixedAccountPrivateKey), + TestConstants.getEdPrivateKey().naturalBytes(), + "pwSmRvZy1c55Kb5tCpBZyq41noSmPn7ynFzUHu1MaoGLAP1VfrT" + ); + } + + /** + * These values come from the rippled codebase in Seed_test.cpp. + */ + @Test + public void encodeDecodeAccountPrivateKeyFromRippled() { + Seed seed = Seed.secp256k1SeedFromPassphrase(Passphrase.of("masterpassphrase")); + + testEncodeDecode( + prefixedNodePrivateKey -> privateKeyCodec.encodeAccountPrivateKey(prefixedNodePrivateKey), + prefixedNodePrivateKey -> privateKeyCodec.decodeAccountPrivateKey(prefixedNodePrivateKey), + seed.deriveKeyPair().privateKey().naturalBytes(), + "p9JfM6HHi64m6mvB6v5k7G2b1cXzGmYiCNJf6GHPKvFTWdeRVjh" + ); + } + + @Test + public void encodeDecodeEcNodePrivate() { + testEncodeDecode( + prefixedNodePrivate -> privateKeyCodec.encodeNodePrivateKey(prefixedNodePrivate), + prefixedNodePrivate -> privateKeyCodec.decodeNodePrivateKey(prefixedNodePrivate), + TestConstants.getEcPrivateKey().naturalBytes(), + "pa1UHARsPMiuDqrJLwFhzcJokoHgyiuaxgPhUGYhkG5ArCfG2vt" + ); + } + + @Test + public void encodeDecodeEcAccountPrivateKey() { + testEncodeDecode( + prefixedAccountPrivateKey -> privateKeyCodec.encodeAccountPrivateKey(prefixedAccountPrivateKey), + prefixedNodePrivateKey -> privateKeyCodec.decodeAccountPrivateKey(prefixedNodePrivateKey), + TestConstants.getEcPrivateKey().naturalBytes(), + "pwkgeQKfaDDMV7w59LhhuEWMbpX3HG2iXxXGgZuij2j6z1RYY7n" + ); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArrayTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArrayTest.java index 3512d66aa..f2d6badd7 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArrayTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArrayTest.java @@ -22,9 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray.of; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -38,27 +36,35 @@ public class UnsignedByteArrayTest { static byte MAX_BYTE = (byte) 255; - @Test - public void ofByteArray() { + ///////// + // of(byte[]) + ///////// - assertThat(of(new byte[] {0}).hexValue()).isEqualTo("00"); - assertThat(of(new byte[] {MAX_BYTE}).hexValue()).isEqualTo("FF"); - assertThat(of(new byte[] {0, MAX_BYTE}).hexValue()).isEqualTo("00FF"); - assertThat(of(new byte[] {MAX_BYTE, 0}).hexValue()).isEqualTo("FF00"); - assertThat(of(new byte[] {MAX_BYTE, MAX_BYTE}).hexValue()).isEqualTo("FFFF"); + @Test + void ofByteArray() { + assertThat(UnsignedByteArray.of(new byte[] {0}).hexValue()).isEqualTo("00"); + assertThat(UnsignedByteArray.of(new byte[] {MAX_BYTE}).hexValue()).isEqualTo("FF"); + assertThat(UnsignedByteArray.of(new byte[] {0, MAX_BYTE}).hexValue()).isEqualTo("00FF"); + assertThat(UnsignedByteArray.of(new byte[] {MAX_BYTE, 0}).hexValue()).isEqualTo("FF00"); + assertThat(UnsignedByteArray.of(new byte[] {MAX_BYTE, MAX_BYTE}).hexValue()).isEqualTo("FFFF"); } + ///////// + // of(UnsignedByteArray) + ///////// + @Test - public void ofUnsignedByteArray() { - assertThat(of(UnsignedByte.of(0)).hexValue()).isEqualTo("00"); - assertThat(of(UnsignedByte.of(MAX_BYTE)).hexValue()).isEqualTo("FF"); - assertThat(of(UnsignedByte.of(0), UnsignedByte.of((MAX_BYTE))).hexValue()).isEqualTo("00FF"); - assertThat(of(UnsignedByte.of(MAX_BYTE), UnsignedByte.of((0))).hexValue()).isEqualTo("FF00"); - assertThat(of(UnsignedByte.of(MAX_BYTE), UnsignedByte.of((MAX_BYTE))).hexValue()).isEqualTo("FFFF"); + void ofUnsignedByteArray() { + assertThat(UnsignedByteArray.of(UnsignedByte.of(0)).hexValue()).isEqualTo("00"); + assertThat(UnsignedByteArray.of(UnsignedByte.of(MAX_BYTE)).hexValue()).isEqualTo("FF"); + assertThat(UnsignedByteArray.of(UnsignedByte.of(0), UnsignedByte.of((MAX_BYTE))).hexValue()).isEqualTo("00FF"); + assertThat(UnsignedByteArray.of(UnsignedByte.of(MAX_BYTE), UnsignedByte.of((0))).hexValue()).isEqualTo("FF00"); + assertThat(UnsignedByteArray.of(UnsignedByte.of(MAX_BYTE), UnsignedByte.of((MAX_BYTE))).hexValue()) + .isEqualTo("FFFF"); } @Test - public void lowerCaseOrUpperCase() { + void lowerCaseOrUpperCase() { assertThat(UnsignedByteArray.fromHex("Ff").hexValue()).isEqualTo("FF"); assertThat(UnsignedByteArray.fromHex("00fF").hexValue()).isEqualTo("00FF"); assertThat(UnsignedByteArray.fromHex("00ff").hexValue()).isEqualTo("00FF"); @@ -70,44 +76,44 @@ public void lowerCaseOrUpperCase() { } @Test - public void empty() { - assertThat(UnsignedByteArray.empty()).isEqualTo(of(new byte[] {})); + void empty() { + assertThat(UnsignedByteArray.empty()).isEqualTo(UnsignedByteArray.of(new byte[] {})); assertThat(UnsignedByteArray.empty().length()).isEqualTo(0); assertThat( - UnsignedByteArray.empty().equals(of(new byte[] {})) + UnsignedByteArray.empty().equals(UnsignedByteArray.of(new byte[] {})) ).isTrue(); } @Test - public void length() { + void length() { final int size = 2; - assertThat(of(new byte[] {0, MAX_BYTE}).length()).isEqualTo(size); - assertThat(of(new byte[] {0, 1}).length()).isEqualTo(UnsignedByteArray.ofSize(size).length()); + assertThat(UnsignedByteArray.of(new byte[] {0, MAX_BYTE}).length()).isEqualTo(size); + assertThat(UnsignedByteArray.of(new byte[] {0, 1}).length()).isEqualTo(UnsignedByteArray.ofSize(size).length()); assertThat(UnsignedByteArray.ofSize(size).length()).isEqualTo(size); } @Test - public void ofSize() { + void ofSize() { final int size = 2; - assertThat(UnsignedByteArray.ofSize(size)).isEqualTo(of(new byte[] {0, 0})); + assertThat(UnsignedByteArray.ofSize(size)).isEqualTo(UnsignedByteArray.of(new byte[] {0, 0})); assertThat(UnsignedByteArray.ofSize(size).length()).isEqualTo(size); - assertThat(UnsignedByteArray.ofSize(size).length()).isEqualTo(of(new byte[] {0, 0}).length()); - assertThat(UnsignedByteArray.ofSize(size).equals(of(new byte[] {0, 0}))).isTrue(); + assertThat(UnsignedByteArray.ofSize(size).length()).isEqualTo(UnsignedByteArray.of(new byte[] {0, 0}).length()); + assertThat(UnsignedByteArray.ofSize(size).equals(UnsignedByteArray.of(new byte[] {0, 0}))).isTrue(); } @Test - public void get() { + void get() { byte[] byteArray = new byte[] {0, 1, 2}; - UnsignedByteArray array = of(byteArray); + UnsignedByteArray array = UnsignedByteArray.of(byteArray); assertThat(array.get(0)).isEqualTo(array.getUnsignedBytes().get(0)); assertThat(array.get(0).asInt()).isEqualTo(byteArray[0]); assertThat(array.get(1).asInt()).isEqualTo(byteArray[1]); } @Test - public void appendByte() { - UnsignedByteArray array1 = of(new byte[] {0, 1}); - UnsignedByteArray array2 = of(new byte[] {0, 1, 9}); + void appendByte() { + UnsignedByteArray array1 = UnsignedByteArray.of(new byte[] {0, 1}); + UnsignedByteArray array2 = UnsignedByteArray.of(new byte[] {0, 1, 9}); int initialLength = array1.length(); assertThat(array1.append(UnsignedByte.of(9))).isEqualTo(array2); assertThat(array1.length() - 1).isEqualTo(initialLength); @@ -115,17 +121,17 @@ public void appendByte() { } @Test - public void appendByteArray() { - UnsignedByteArray array1 = of(new byte[] {0, 1}); - UnsignedByteArray array2 = of(new byte[] {0, 1, 8, 9}); + void appendByteArray() { + UnsignedByteArray array1 = UnsignedByteArray.of(new byte[] {0, 1}); + UnsignedByteArray array2 = UnsignedByteArray.of(new byte[] {0, 1, 8, 9}); int initialLength = array1.length(); - assertThat(array1.append(of(new byte[] {8, 9}))).isEqualTo(array2); + assertThat(array1.append(UnsignedByteArray.of(new byte[] {8, 9}))).isEqualTo(array2); assertThat(array1.length()).isEqualTo(initialLength + 2); assertThat(array2.length()).isEqualTo(initialLength + 2); } @Test - public void fill() { + void fill() { List unsignedBytes1 = new ArrayList<>(); List unsignedBytes2 = Arrays.asList(UnsignedByte.of(0), UnsignedByte.of(0)); List filledBytes = UnsignedByteArray.fill(2); @@ -136,46 +142,46 @@ public void fill() { } @Test - public void set() { - UnsignedByteArray array1 = of(new byte[] {0, 1}); - UnsignedByteArray array2 = of(new byte[] {0, 9}); + void set() { + UnsignedByteArray array1 = UnsignedByteArray.of(new byte[] {0, 1}); + UnsignedByteArray array2 = UnsignedByteArray.of(new byte[] {0, 9}); assertThat(array1).isNotEqualTo(array2); array1.set(1, UnsignedByte.of(9)); assertThat(array1).isEqualTo(array2); } @Test - public void slice() { - UnsignedByteArray array1 = of(new byte[] {0, 8, 9, 1}); - UnsignedByteArray array2 = of(new byte[] {8, 9}); + void slice() { + UnsignedByteArray array1 = UnsignedByteArray.of(new byte[] {0, 8, 9, 1}); + UnsignedByteArray array2 = UnsignedByteArray.of(new byte[] {8, 9}); assertThat(array1).isNotEqualTo(array2); assertThat(array1.slice(1, 3)).isEqualTo(array2); assertThrows(IndexOutOfBoundsException.class, () -> array1.slice(1, 5)); } @Test - public void hashcode() { - UnsignedByteArray array1 = of(new byte[] {0, 1}); - assertThat(array1).isNotEqualTo(of(new byte[] {8, 9})); - assertThat(array1.hashCode()).isNotEqualTo(of(new byte[] {8, 9}).hashCode()); + void hashcode() { + UnsignedByteArray array1 = UnsignedByteArray.of(new byte[] {0, 1}); + assertThat(array1).isNotEqualTo(UnsignedByteArray.of(new byte[] {8, 9})); + assertThat(array1.hashCode()).isNotEqualTo(UnsignedByteArray.of(new byte[] {8, 9}).hashCode()); assertThat(array1.hashCode()).isEqualTo(array1.hashCode()); - assertThat(array1.hashCode()).isEqualTo(of(new byte[] {0, 1}).hashCode()); + assertThat(array1.hashCode()).isEqualTo(UnsignedByteArray.of(new byte[] {0, 1}).hashCode()); } @Test - public void unsignedByteArrayToString() { - UnsignedByteArray array1 = of(new byte[] {0, 1}); - UnsignedByteArray array2 = of(new byte[] {8, 9}); + void unsignedByteArrayToString() { + UnsignedByteArray array1 = UnsignedByteArray.of(new byte[] {0, 1}); + UnsignedByteArray array2 = UnsignedByteArray.of(new byte[] {8, 9}); assertThat(array1.toString()).isEqualTo(array1.toString()); assertThat(array1).isNotEqualTo(array2); assertThat(array1.toString()).isEqualTo(array2.toString()); } @Test - public void unsignedByteArrayEqualsTest() { - UnsignedByteArray array1 = of(new byte[] {0, MAX_BYTE}); - UnsignedByteArray array2 = of(new byte[] {MAX_BYTE, 0}); - UnsignedByteArray array3 = of(new byte[] {0, MAX_BYTE}); + void unsignedByteArrayEqualsTest() { + UnsignedByteArray array1 = UnsignedByteArray.of(new byte[] {0, MAX_BYTE}); + UnsignedByteArray array2 = UnsignedByteArray.of(new byte[] {MAX_BYTE, 0}); + UnsignedByteArray array3 = UnsignedByteArray.of(new byte[] {0, MAX_BYTE}); UnsignedByteArray array4 = array1; assertThat(array1.equals(array1)).isTrue(); @@ -194,7 +200,7 @@ public void unsignedByteArrayEqualsTest() { @Test void destroy() { - UnsignedByteArray uba = of(new byte[] {0, MAX_BYTE}); + UnsignedByteArray uba = UnsignedByteArray.of(new byte[] {0, MAX_BYTE}); uba.destroy(); assertThat(uba.isDestroyed()).isTrue(); assertThat(uba.toByteArray()).isEqualTo(new byte[0]); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteTest.java index 17f394fce..8c6c94039 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteTest.java @@ -21,15 +21,27 @@ */ import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; +import org.mockito.internal.matchers.Null; import java.math.BigInteger; +/** + * Unit tests for {@link UnsignedByte}. + */ public class UnsignedByteTest { @Test - public void hexValue() { + void constructorTests() { + assertThrows(IllegalArgumentException.class, () -> UnsignedByte.of(-1)); + assertThrows(NullPointerException.class, () -> UnsignedByte.of((UnsignedByte) null)); + assertThat(UnsignedByte.of(UnsignedByte.of(0))).isEqualTo(UnsignedByte.of(0)); + } + + @Test + void hexValue() { assertThat(UnsignedByte.of(0).hexValue()).isEqualTo("00"); assertThat(UnsignedByte.of(127).hexValue()).isEqualTo("7F"); assertThat(UnsignedByte.of(128).hexValue()).isEqualTo("80"); @@ -38,7 +50,7 @@ public void hexValue() { } @Test - public void isNthBitSetAllZero() { + void isNthBitSetAllZero() { UnsignedByte value = UnsignedByte.of(0); for (int i = 1; i <= 8; i++) { assertThat(value.isNthBitSet(i)).isFalse(); @@ -46,7 +58,7 @@ public void isNthBitSetAllZero() { } @Test - public void isNthBitSetAllSet() { + void isNthBitSetAllSet() { UnsignedByte value = UnsignedByte.of(new BigInteger("11111111", 2).intValue()); for (int i = 1; i <= 8; i++) { assertThat(value.isNthBitSet(i)).isTrue(); @@ -54,7 +66,7 @@ public void isNthBitSetAllSet() { } @Test - public void isNthBitSetEveryOther() { + void isNthBitSetEveryOther() { UnsignedByte value = UnsignedByte.of(new BigInteger("10101010", 2).intValue()); assertThat(value.isNthBitSet(1)).isTrue(); assertThat(value.isNthBitSet(2)).isFalse(); @@ -67,7 +79,7 @@ public void isNthBitSetEveryOther() { } @Test - public void intValue() { + void intValue() { assertThat(UnsignedByte.of(0x00).asInt()).isEqualTo(0); assertThat(UnsignedByte.of(0x0F).asInt()).isEqualTo(15); assertThat(UnsignedByte.of(0xFF).asInt()).isEqualTo(255); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/FieldHeaderCodecTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/FieldHeaderCodecTest.java index dc476683d..717c80019 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/FieldHeaderCodecTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/FieldHeaderCodecTest.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. diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/XrplBinaryCodecTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/XrplBinaryCodecTest.java index 4b581dbb3..cf394bb73 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/XrplBinaryCodecTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/XrplBinaryCodecTest.java @@ -28,11 +28,15 @@ import com.google.common.primitives.UnsignedInteger; import org.assertj.core.api.Assertions; import org.assertj.core.util.Lists; +import org.json.JSONException; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; import org.xrpl.xrpl4j.codec.fixtures.FixtureUtils; +import org.xrpl.xrpl4j.codec.fixtures.codec.CodecFixture; import org.xrpl.xrpl4j.codec.fixtures.data.WholeObject; import org.xrpl.xrpl4j.crypto.keys.PublicKey; import org.xrpl.xrpl4j.crypto.signing.Signature; @@ -71,6 +75,10 @@ private static Stream dataDrivenFixtures() throws IOException { .map(Arguments::of); } + private static Stream transactionCodecFixtures() throws IOException { + return FixtureUtils.getCodecFixtures().transactions().stream().map(Arguments::of); + } + @Test void encodeDecodeSimple() throws JsonProcessingException { assertThat(encoder.encode(SIMPLE_JSON)).isEqualTo(SIMPLE_HEX); @@ -452,4 +460,13 @@ void dataDriven(WholeObject wholeObject) throws IOException { assertThat(encoder.encode(wholeObject.txJson().toString())).isEqualTo(wholeObject.expectedHex()); } + @ParameterizedTest + @MethodSource("transactionCodecFixtures") + void transactionFixtureTests(CodecFixture codecFixture) throws JsonProcessingException, JSONException { + assertThat(encoder.encode(codecFixture.json().toString())).isEqualTo(codecFixture.binary()); + JSONAssert.assertEquals( + encoder.decode(codecFixture.binary()), codecFixture.json().toString(), JSONCompareMode.STRICT + ); + } + } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/TestConstants.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/TestConstants.java index 2abd02fb5..534fb6d5f 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/TestConstants.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/TestConstants.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. @@ -21,10 +21,12 @@ */ import com.google.common.io.BaseEncoding; +import org.xrpl.xrpl4j.codec.addresses.KeyType; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.crypto.keys.PrivateKey; import org.xrpl.xrpl4j.crypto.keys.PublicKey; import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Hash256; /** * Constants used for testing. @@ -36,20 +38,46 @@ public interface TestConstants { String ED_PUBLIC_KEY_B58 = "aKEusmsH9dJvjfeEg8XhDfpEgmhcK1epAtFJfAQbACndz5mUA73B"; PublicKey ED_PUBLIC_KEY = PublicKey.fromBase16EncodedPublicKey(ED_PUBLIC_KEY_HEX); - String ED_PRIVATE_KEY_HEX = "EDB224AFDCCEC7AA4E245E35452585D4FBBE37519BCA3929578BFC5BBD4640E163"; - String ED_PRIVATE_KEY_B58 = "pDcQTi2uFBAzQ7cY2mYQtk9QuQBoLU6rJypEf8EYPQoouh"; - PrivateKey ED_PRIVATE_KEY = PrivateKey.of(UnsignedByteArray.of(BaseEncoding.base16().decode(ED_PRIVATE_KEY_HEX))); + String ED_PRIVATE_KEY_HEX = "B224AFDCCEC7AA4E245E35452585D4FBBE37519BCA3929578BFC5BBD4640E163"; + String ED_PRIVATE_KEY_WITH_PREFIX_HEX = "ED" + ED_PRIVATE_KEY_HEX; + + /** + * Helper method to return a newly constructed {@link PrivateKey} with its own copy of bytes. + * + * @return A {@link PrivateKey}. + */ + static PrivateKey getEdPrivateKey() { + return PrivateKey.fromNaturalBytes( + UnsignedByteArray.of(BaseEncoding.base16().decode(ED_PRIVATE_KEY_HEX)), + KeyType.ED25519 + ); + } // Secp256k1 Public Key String EC_PUBLIC_KEY_HEX = "027535A4E90B2189CF9885563F45C4F454B3BFAB21930089C3878A9427B4D648D9"; String EC_PUBLIC_KEY_B58 = "aB4ifx88a26RYRSSzeKW8HpbXfbpzQFRsX6dMNmMwEVHUTKzfWdk"; PublicKey EC_PUBLIC_KEY = PublicKey.fromBase16EncodedPublicKey(EC_PUBLIC_KEY_HEX); - String EC_PRIVATE_KEY_HEX = "00DAD3C2B4BF921398932C889DE5335F89D90249355FC6FFB73F1256D2957F9F17"; - String EC_PRIVATE_KEY_B58 = "rEjDwJp2Pm3NrUtcf8v17jWopvqPJxyi5RTrDfhcJcWSi"; - PrivateKey EC_PRIVATE_KEY = PrivateKey.of(UnsignedByteArray.of(BaseEncoding.base16().decode(EC_PRIVATE_KEY_HEX))); + String EC_PRIVATE_KEY_HEX = "DAD3C2B4BF921398932C889DE5335F89D90249355FC6FFB73F1256D2957F9F17"; + String EC_PRIVATE_KEY_WITH_PREFIX_HEX = "00" + EC_PRIVATE_KEY_HEX; + + /** + * Helper method to return a newly constructed {@link PrivateKey} with its own copy of bytes. + * + * @return A {@link PrivateKey}. + */ + static PrivateKey getEcPrivateKey() { + return PrivateKey.fromNaturalBytes( + UnsignedByteArray.of(BaseEncoding.base16().decode(EC_PRIVATE_KEY_HEX)), KeyType.SECP256K1 + ); + } // Both generated from Passphrase.of("hello") Address ED_ADDRESS = Address.of("rwGWYtRR6jJJJq7FKQg74YwtkiPyUqJ466"); Address EC_ADDRESS = Address.of("rD8ATvjj9mfnFuYYTGRNb9DygnJW9JNN1C"); + + /** + * A sample {@link Hash256}. + */ + Hash256 HASH_256 = Hash256.of("6B1011EF3BC3ED619B15979EF75C1C60D9181F3DDE641AD3019318D3900CEE2E"); } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/PrivateKeyTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/PrivateKeyTest.java index 57145e87a..9900d87eb 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/PrivateKeyTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/PrivateKeyTest.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. @@ -21,95 +21,468 @@ */ import static org.assertj.core.api.Assertions.assertThat; -import static org.xrpl.xrpl4j.crypto.TestConstants.EC_PRIVATE_KEY; -import static org.xrpl.xrpl4j.crypto.TestConstants.ED_PRIVATE_KEY; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.xrpl.xrpl4j.crypto.TestConstants.EC_PRIVATE_KEY_HEX; +import static org.xrpl.xrpl4j.crypto.TestConstants.EC_PRIVATE_KEY_WITH_PREFIX_HEX; +import static org.xrpl.xrpl4j.crypto.TestConstants.ED_PRIVATE_KEY_HEX; +import static org.xrpl.xrpl4j.crypto.TestConstants.ED_PRIVATE_KEY_WITH_PREFIX_HEX; +import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; import org.junit.jupiter.api.Test; -import org.xrpl.xrpl4j.codec.addresses.Base58; import org.xrpl.xrpl4j.codec.addresses.KeyType; +import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.crypto.TestConstants; +import org.xrpl.xrpl4j.crypto.signing.bc.Secp256k1; /** * Unit tests for {@link PrivateKey}. */ -public class PrivateKeyTest { +@SuppressWarnings("deprecation") +class PrivateKeyTest { + + //////////////////// + // of + //////////////////// + + @Test + void testOfWithNull() { + assertThrows(NullPointerException.class, () -> PrivateKey.of(null)); + } + + @Test + void testEcOf() { + UnsignedByteArray thirtyThreeBytes = UnsignedByteArray.of( + BaseEncoding.base16().decode(EC_PRIVATE_KEY_WITH_PREFIX_HEX)); + assertThat(PrivateKey.of(thirtyThreeBytes)).isEqualTo(TestConstants.getEcPrivateKey()); + + UnsignedByteArray thirtyTwoBytes = UnsignedByteArray.of(thirtyThreeBytes.slice(1, 33).toByteArray()); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.of(thirtyTwoBytes) + ); + assertThat(exception.getMessage()).isEqualTo( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but 32 were supplied." + ); + } + + @Test + void testEdOf() { + UnsignedByteArray thirtyThreeBytes = UnsignedByteArray.of( + BaseEncoding.base16().decode(ED_PRIVATE_KEY_WITH_PREFIX_HEX)); + assertThat(PrivateKey.of(thirtyThreeBytes)).isEqualTo(TestConstants.getEdPrivateKey()); + + UnsignedByteArray thirtyTwoBytes = UnsignedByteArray.of(thirtyThreeBytes.slice(1, 33).toByteArray()); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.of(thirtyTwoBytes) + ); + assertThat(exception.getMessage()).isEqualTo( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but 32 were supplied." + ); + } + + @Test + void testOfWithLessWithEmpty() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.of(UnsignedByteArray.empty()) + ); + assertThat(exception.getMessage()) + .isEqualTo("The `fromPrefixedBytes` function requires input length of 33 bytes, but 0 were supplied."); + } + + @Test + void testEdOfWithLessThan32Bytes() { + UnsignedByteArray twoBytes = UnsignedByteArray.of(new byte[] {(byte) 0xED, (byte) 0xFF}); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.of(twoBytes) + ); + assertThat(exception.getMessage()).isEqualTo( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but 2 were supplied." + ); + } + + @Test + void testEcOfWithLessThan32Bytes() { + UnsignedByteArray twoBytes = UnsignedByteArray.of(new byte[] {(byte) 0x00, (byte) 0xFF}); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.of(twoBytes) + ); + assertThat(exception.getMessage()).isEqualTo( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but 2 were supplied." + ); + } + + /////////////////// + // fromNaturalBytes + /////////////////// + + @Test + void testFromNaturalBytesWithNull() { + assertThrows(NullPointerException.class, () -> PrivateKey.fromNaturalBytes(null, KeyType.ED25519)); + assertThrows(NullPointerException.class, () -> PrivateKey.fromNaturalBytes(UnsignedByteArray.empty(), null)); + } + + @Test + void testEcFromNaturalBytesWithEmpty() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromNaturalBytes(UnsignedByteArray.empty(), KeyType.SECP256K1) + ); + assertThat(exception.getMessage()) + .isEqualTo("Byte values passed to this constructor must be 32 bytes long, with no prefix."); + } + + @Test + void testEdFromNaturalBytesWithEmpty() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromNaturalBytes(UnsignedByteArray.empty(), KeyType.ED25519) + ); + assertThat(exception.getMessage()) + .isEqualTo("Byte values passed to this constructor must be 32 bytes long, with no prefix."); + } + + @Test + void testEcFromNaturalBytes() { + UnsignedByteArray thirtyThreeBytes = UnsignedByteArray.of( + BaseEncoding.base16().decode(EC_PRIVATE_KEY_WITH_PREFIX_HEX) + ); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromNaturalBytes(thirtyThreeBytes, KeyType.SECP256K1) + ); + assertThat(exception.getMessage()) + .isEqualTo("Byte values passed to this constructor must be 32 bytes long, with no prefix."); + + UnsignedByteArray thirtyTwoBytes = UnsignedByteArray.of(thirtyThreeBytes.slice(1, 33).toByteArray()); + assertThat(PrivateKey.fromNaturalBytes(thirtyTwoBytes, KeyType.SECP256K1)).isEqualTo( + TestConstants.getEcPrivateKey() + ); + } + + @Test + void testEdFromNaturalBytes() { + UnsignedByteArray thirtyThreeBytes = UnsignedByteArray.of( + BaseEncoding.base16().decode(ED_PRIVATE_KEY_WITH_PREFIX_HEX)); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromNaturalBytes(thirtyThreeBytes, KeyType.ED25519) + ); + assertThat(exception.getMessage()) + .isEqualTo("Byte values passed to this constructor must be 32 bytes long, with no prefix."); + + UnsignedByteArray thirtyTwoBytes = UnsignedByteArray.of(thirtyThreeBytes.slice(1, 33).toByteArray()); + assertThat(PrivateKey.fromNaturalBytes(thirtyTwoBytes, KeyType.ED25519)).isEqualTo(TestConstants.getEdPrivateKey()); + } + + @Test + void testEdFromNaturalBytesWithLessThan32Bytes() { + UnsignedByteArray twoBytes = UnsignedByteArray.of(new byte[] {(byte) 0xED, (byte) 0xFF}); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromNaturalBytes(twoBytes, KeyType.ED25519) + ); + assertThat(exception.getMessage()) + .isEqualTo("Byte values passed to this constructor must be 32 bytes long, with no prefix."); + } + + @Test + void testEcFromNaturalBytesWithLessThan32Bytes() { + UnsignedByteArray twoBytes = UnsignedByteArray.of(new byte[] {(byte) 0x00, (byte) 0xFF}); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromNaturalBytes(twoBytes, KeyType.SECP256K1) + ); + assertThat(exception.getMessage()) + .isEqualTo("Byte values passed to this constructor must be 32 bytes long, with no prefix."); + } + + //////////////////// + // fromPrefixedBytes + //////////////////// + + @Test + void testFromPrefixedBytesWithNull() { + assertThrows(NullPointerException.class, () -> PrivateKey.fromPrefixedBytes(null)); + } + + @Test + void testEcFromPrefixedBytes() { + UnsignedByteArray thirtyThreeBytes = UnsignedByteArray.of( + BaseEncoding.base16().decode(EC_PRIVATE_KEY_WITH_PREFIX_HEX) + ); + assertThat(PrivateKey.fromPrefixedBytes(thirtyThreeBytes)).isEqualTo(TestConstants.getEcPrivateKey()); + + UnsignedByteArray thirtyTwoBytes = UnsignedByteArray.of(thirtyThreeBytes.slice(1, 33).toByteArray()); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromPrefixedBytes(thirtyTwoBytes) + ); + assertThat(exception.getMessage()).isEqualTo( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but 32 were supplied." + ); + } + + @Test + void testEdFromPrefixedBytes() { + UnsignedByteArray thirtyThreeBytes = UnsignedByteArray.of( + BaseEncoding.base16().decode(ED_PRIVATE_KEY_WITH_PREFIX_HEX) + ); + assertThat(PrivateKey.fromPrefixedBytes(thirtyThreeBytes)).isEqualTo(TestConstants.getEdPrivateKey()); + + UnsignedByteArray thirtyTwoBytes = UnsignedByteArray.of(thirtyThreeBytes.slice(1, 33).toByteArray()); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromPrefixedBytes(thirtyTwoBytes) + ); + assertThat(exception.getMessage()).isEqualTo( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but 32 were supplied." + ); + } + + @Test + void testEdFromPrefixedBytesWithLessThan32Bytes() { + UnsignedByteArray twoBytes = UnsignedByteArray.of(new byte[] {(byte) 0xED, (byte) 0xFF}); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromPrefixedBytes(twoBytes) + ); + assertThat(exception.getMessage()).isEqualTo( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but 2 were supplied." + ); + } + + @Test + void testEcFromPrefixedBytesWithLessThan32Bytes() { + UnsignedByteArray twoBytes = UnsignedByteArray.of(new byte[] {(byte) 0x00, (byte) 0xFF}); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromPrefixedBytes(twoBytes) + ); + assertThat(exception.getMessage()).isEqualTo( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but 2 were supplied." + ); + } + + @Test + void testEdFromPrefixedBytesWith0Bytes() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromPrefixedBytes(UnsignedByteArray.empty()) + ); + assertThat(exception.getMessage()).isEqualTo( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but 0 were supplied." + ); + } + + @Test + void testEcFromPrefixedBytesWithLess0Bytes() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromPrefixedBytes(UnsignedByteArray.empty()) + ); + assertThat(exception.getMessage()).isEqualTo( + "The `fromPrefixedBytes` function requires input length of 33 bytes, but 0 were supplied." + ); + } + + @Test + void testFromPrefixedBytesWithInvalidPrefix() { + final byte[] invalidPrefixBytes = BaseEncoding.base16().decode( + "20000000000000000000000000000000" + // <-- 16 bytes + "0000000000000000000000000000000000"); // <-- 17 bytes + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> PrivateKey.fromPrefixedBytes(UnsignedByteArray.of(invalidPrefixBytes)) + ); + assertThat(exception.getMessage()).isEqualTo( + "PrivateKey construction requires 32 natural bytes plug a one-byte prefix value of either `0xED` for " + + "ed25519 private keys or `0x00` for secp256k1 private keys. Input byte length was 33 bytes with a prefixByte " + + "value of `0x20`" + ); + } + + /////////////////// + // Constants + /////////////////// @Test - public void valueEd25519() { - assertThat(Base58.encode(ED_PRIVATE_KEY.value().toByteArray())).isEqualTo(TestConstants.ED_PRIVATE_KEY_B58); + void testConstants() { + assertThat(PrivateKey.ED2559_PREFIX.asInt()).isEqualTo(0xED); + assertThat(PrivateKey.ED2559_PREFIX.asByte()).isEqualTo((byte) 0xED); + + assertThat(PrivateKey.SECP256K1_PREFIX.asInt()).isEqualTo(0x00); + assertThat(PrivateKey.SECP256K1_PREFIX.asByte()).isEqualTo((byte) 0x00); } + /////////////////// + // value tests ==> [value, valueWithPrefixedBytes, valueWithNaturalBytes] + /////////////////// + @Test - public void valueSecp256k1() { - assertThat(Base58.encode(EC_PRIVATE_KEY.value().toByteArray())).isEqualTo(TestConstants.EC_PRIVATE_KEY_B58); + void valueForEd25519() { + assertThat(TestConstants.getEdPrivateKey().value().hexValue()) // <-- Overtly test .value() + .isEqualTo(ED_PRIVATE_KEY_WITH_PREFIX_HEX); + assertThat(TestConstants.getEdPrivateKey().prefixedBytes().hexValue()).isEqualTo( + ED_PRIVATE_KEY_WITH_PREFIX_HEX + ); + assertThat(TestConstants.getEdPrivateKey().naturalBytes().hexValue()).isEqualTo(ED_PRIVATE_KEY_HEX); } @Test - public void keyTypeEd25519() { - assertThat(ED_PRIVATE_KEY.keyType()).isEqualTo(KeyType.ED25519); + void valueForSecp256k1() { + assertThat(TestConstants.getEcPrivateKey().value().hexValue()) // <-- Overtly test .value() + .isEqualTo(EC_PRIVATE_KEY_WITH_PREFIX_HEX); + assertThat(TestConstants.getEcPrivateKey().prefixedBytes().hexValue()).isEqualTo( + EC_PRIVATE_KEY_WITH_PREFIX_HEX + ); + assertThat(TestConstants.getEcPrivateKey().naturalBytes().hexValue()).isEqualTo(EC_PRIVATE_KEY_HEX); } + /////////////////// + // Misc + /////////////////// + @Test - public void keyTypeSecp256k1() { - assertThat(EC_PRIVATE_KEY.keyType()).isEqualTo(KeyType.SECP256K1); + void keyTypeEd25519() { + assertThat(TestConstants.getEdPrivateKey().keyType()).isEqualTo(KeyType.ED25519); + } + + @Test + void keyTypeSecp256k1() { + assertThat(TestConstants.getEcPrivateKey().keyType()).isEqualTo(KeyType.SECP256K1); } @Test void destroy() { - PrivateKey privateKey = PrivateKey.of(ED_PRIVATE_KEY.value()); + PrivateKey privateKey = PrivateKey.fromPrefixedBytes(TestConstants.getEdPrivateKey().prefixedBytes()); assertThat(privateKey.isDestroyed()).isFalse(); privateKey.destroy(); assertThat(privateKey.isDestroyed()).isTrue(); - assertThat(privateKey.value().hexValue()).isEqualTo(""); + assertThat(privateKey.prefixedBytes().hexValue()).isEqualTo(""); privateKey.destroy(); assertThat(privateKey.isDestroyed()).isTrue(); - assertThat(privateKey.value().hexValue()).isEqualTo(""); + assertThat(privateKey.value().hexValue()).isEqualTo(""); // <-- Overtly test .value() + assertThat(privateKey.naturalBytes().hexValue()).isEqualTo(""); + assertThat(privateKey.prefixedBytes().hexValue()).isEqualTo(""); + + assertThat(privateKey.value()).isEqualTo(UnsignedByteArray.empty()); // <-- Overtly test .value() + assertThat(privateKey.naturalBytes()).isEqualTo(UnsignedByteArray.empty()); + assertThat(privateKey.prefixedBytes()).isEqualTo(UnsignedByteArray.empty()); - privateKey = PrivateKey.of(EC_PRIVATE_KEY.value()); + privateKey = PrivateKey.fromPrefixedBytes(TestConstants.getEcPrivateKey().prefixedBytes()); assertThat(privateKey.isDestroyed()).isFalse(); privateKey.destroy(); assertThat(privateKey.isDestroyed()).isTrue(); - assertThat(privateKey.value().hexValue()).isEqualTo(""); + assertThat(privateKey.prefixedBytes().hexValue()).isEqualTo(""); privateKey.destroy(); assertThat(privateKey.isDestroyed()).isTrue(); - assertThat(privateKey.value().hexValue()).isEqualTo(""); + + assertThat(privateKey.value().hexValue()).isEqualTo(""); // <-- Overtly test .value() + assertThat(privateKey.naturalBytes().hexValue()).isEqualTo(""); + assertThat(privateKey.prefixedBytes().hexValue()).isEqualTo(""); + + assertThat(privateKey.value()).isEqualTo(UnsignedByteArray.empty()); // <-- Overtly test .value() + assertThat(privateKey.naturalBytes()).isEqualTo(UnsignedByteArray.empty()); + assertThat(privateKey.prefixedBytes()).isEqualTo(UnsignedByteArray.empty()); } @Test void equals() { - assertThat(ED_PRIVATE_KEY).isEqualTo(ED_PRIVATE_KEY); - assertThat(ED_PRIVATE_KEY).isNotEqualTo(EC_PRIVATE_KEY); - assertThat(EC_PRIVATE_KEY).isNotEqualTo(new Object()); + PrivateKey privateKey = TestConstants.getEdPrivateKey(); + assertThat(privateKey).isEqualTo(privateKey); // <-- To cover reference equality in .equals + + assertThat(TestConstants.getEdPrivateKey()).isEqualTo(TestConstants.getEdPrivateKey()); + assertThat(TestConstants.getEdPrivateKey()).isEqualTo( + PrivateKey.fromPrefixedBytes(TestConstants.getEdPrivateKey().prefixedBytes()) + ); + assertThat(TestConstants.getEdPrivateKey()).isEqualTo( + PrivateKey.fromNaturalBytes(TestConstants.getEdPrivateKey().naturalBytes(), KeyType.ED25519) + ); + assertThat(TestConstants.getEdPrivateKey()).isNotEqualTo(TestConstants.getEcPrivateKey()); + assertThat(TestConstants.getEcPrivateKey()).isNotEqualTo(new Object()); - assertThat(EC_PRIVATE_KEY).isEqualTo(EC_PRIVATE_KEY); - assertThat(EC_PRIVATE_KEY).isNotEqualTo(ED_PRIVATE_KEY); - assertThat(EC_PRIVATE_KEY).isNotEqualTo(new Object()); + assertThat(TestConstants.getEcPrivateKey()).isEqualTo(TestConstants.getEcPrivateKey()); + assertThat(TestConstants.getEcPrivateKey()).isEqualTo( + PrivateKey.fromPrefixedBytes(TestConstants.getEcPrivateKey().prefixedBytes()) + ); + assertThat(TestConstants.getEcPrivateKey()).isEqualTo( + PrivateKey.fromNaturalBytes(TestConstants.getEcPrivateKey().naturalBytes(), KeyType.SECP256K1) + ); + + assertThat(TestConstants.getEcPrivateKey()).isNotEqualTo(TestConstants.getEdPrivateKey()); + assertThat(TestConstants.getEcPrivateKey()).isNotEqualTo(new Object()); } @Test void testHashcode() { - assertThat(ED_PRIVATE_KEY.hashCode()).isEqualTo(ED_PRIVATE_KEY.hashCode()); - assertThat(ED_PRIVATE_KEY.hashCode()).isNotEqualTo(EC_PRIVATE_KEY.hashCode()); + assertThat(TestConstants.getEdPrivateKey().hashCode()).isEqualTo(TestConstants.getEdPrivateKey().hashCode()); + assertThat(TestConstants.getEdPrivateKey().hashCode()).isNotEqualTo(TestConstants.getEcPrivateKey().hashCode()); - assertThat(EC_PRIVATE_KEY.hashCode()).isEqualTo(EC_PRIVATE_KEY.hashCode()); - assertThat(EC_PRIVATE_KEY.hashCode()).isNotEqualTo(ED_PRIVATE_KEY.hashCode()); + assertThat(TestConstants.getEcPrivateKey().hashCode()).isEqualTo(TestConstants.getEcPrivateKey().hashCode()); + assertThat(TestConstants.getEcPrivateKey().hashCode()).isNotEqualTo(TestConstants.getEdPrivateKey().hashCode()); } @Test void testToString() { - assertThat(ED_PRIVATE_KEY.toString()).isEqualTo( + assertThat(TestConstants.getEdPrivateKey().toString()).isEqualTo( "PrivateKey{" + - "value=[redacted], " + + "value=[redacted]," + + "keyType=ED25519," + "destroyed=false" + "}" ); - assertThat(EC_PRIVATE_KEY.toString()).isEqualTo( + assertThat(TestConstants.getEcPrivateKey().toString()).isEqualTo( "PrivateKey{" + - "value=[redacted], " + + "value=[redacted]," + + "keyType=SECP256K1," + "destroyed=false" + "}" ); } + /** + * Ensure that no part of a {@link PrivateKey} is mutable from the outside (except for {@link PrivateKey#destroy()}. + */ + @Test + void testImmutability() { + // Never changes, used to compare later in this test. + final UnsignedByteArray controlBytes = UnsignedByteArray.of( + BaseEncoding.base16().decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + ); + Preconditions.checkArgument(controlBytes.length() == 32); // <-- Start from a known value. + + UnsignedByteArray bytes32 = UnsignedByteArray.of( + BaseEncoding.base16().decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + ); + Preconditions.checkArgument(bytes32.length() == 32); // <-- Start from a known value. + Preconditions.checkArgument(bytes32.equals(controlBytes)); + + /////////////// + // The tests + /////////////// + + // Check that mutating the result of PrivateKey.fromNaturalBytes() doesn't enable inner mutability of the original. + PrivateKey testPrivateKey = PrivateKey.fromNaturalBytes(bytes32, KeyType.ED25519); // <-- The crux of the test + Preconditions.checkArgument(testPrivateKey.naturalBytes().equals(controlBytes)); // <-- Sanity check + testPrivateKey.destroy(); // <-- Set all bytes to 0 + assertThat(testPrivateKey.naturalBytes()).isNotEqualTo(controlBytes); // <-- The test + + // Check that mutating the result of PrivateKey.fromPrefixedBytes() doesn't enable inner mutability of the original. + testPrivateKey = PrivateKey.fromPrefixedBytes( // <-- The crux of the test + Secp256k1.withZeroPrefixPadding(bytes32, 33) + ); + Preconditions.checkArgument(testPrivateKey.naturalBytes().equals(controlBytes)); // <-- Sanity check + testPrivateKey.destroy(); // <-- Set all bytes to 0 + assertThat(testPrivateKey.naturalBytes()).isNotEqualTo(controlBytes); // <-- The test + + // Check that mutating the result of PrivateKey.naturalBytes() doesn't enable inner mutability of the original. + testPrivateKey = PrivateKey.fromNaturalBytes(bytes32, KeyType.SECP256K1); + Preconditions.checkArgument(testPrivateKey.naturalBytes().equals(controlBytes)); // <-- Sanity check + UnsignedByteArray valueBytes = testPrivateKey.naturalBytes(); // <-- The crux of the test + valueBytes.destroy(); // <-- Set all bytes to 0 + assertThat(testPrivateKey.naturalBytes()).isEqualTo(controlBytes); // <-- The test + + // Check that mutating the result of PrivateKey.prefixedBytes() doesn't enable inner mutability of the original. + testPrivateKey = PrivateKey.fromNaturalBytes(bytes32, KeyType.SECP256K1); + Preconditions.checkArgument(testPrivateKey.naturalBytes().equals(controlBytes)); // <-- Sanity check + valueBytes = testPrivateKey.prefixedBytes(); // <-- The crux of the test + valueBytes.destroy(); // <-- Set all bytes to 0 + assertThat(testPrivateKey.naturalBytes()).isEqualTo(controlBytes); // <-- The test + + // Check that PrivateKey.value() doesn't enable inner mutability. + testPrivateKey = PrivateKey.fromNaturalBytes(bytes32, KeyType.SECP256K1); + Preconditions.checkArgument(testPrivateKey.naturalBytes().equals(controlBytes)); // <-- Sanity check + valueBytes = testPrivateKey.value(); // <-- The crux of the test + valueBytes.destroy(); // <-- Set all bytes to 0 + assertThat(testPrivateKey.naturalBytes()).isEqualTo(controlBytes); // <-- The test + } + } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/PublicKeyTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/PublicKeyTest.java index ed9d9b304..0a072fb29 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/PublicKeyTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/PublicKeyTest.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. @@ -21,6 +21,7 @@ */ import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.xrpl.xrpl4j.crypto.TestConstants.EC_PUBLIC_KEY; import static org.xrpl.xrpl4j.crypto.TestConstants.EC_PUBLIC_KEY_B58; import static org.xrpl.xrpl4j.crypto.TestConstants.EC_PUBLIC_KEY_HEX; @@ -32,6 +33,7 @@ import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.codec.addresses.KeyType; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; +import org.xrpl.xrpl4j.codec.addresses.exceptions.EncodeException; import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; import org.xrpl.xrpl4j.model.transactions.Address; @@ -40,6 +42,15 @@ */ public class PublicKeyTest { + @Test + public void fromBase58EncodedStringEd25519WithTooFewBytes() { + UnsignedByteArray twoBytes = UnsignedByteArray.of(new byte[] {(byte) 0xED, (byte) 0xFF}); + EncodeException exception = assertThrows( + EncodeException.class, () -> PublicKey.fromBase16EncodedPublicKey(twoBytes.hexValue()) + ); + assertThat(exception.getMessage()).isEqualTo("Length of bytes does not match expectedLength of 33."); + } + @Test public void fromBase58EncodedStringEd25519() { assertThat(PublicKey.fromBase58EncodedPublicKey(ED_PUBLIC_KEY_B58).base58Value()).isEqualTo(ED_PUBLIC_KEY_B58); @@ -196,7 +207,7 @@ void jsonSerializeAndDeserializeMultiSignKey() throws JsonProcessingException { PublicKey actual = ObjectMapperFactory.create().readValue(json, PublicKey.class); assertThat(actual.base16Value()).isEqualTo(""); } - + @Test void jsonSerializeAndDeserializeEc() throws JsonProcessingException { String json = ObjectMapperFactory.create().writeValueAsString(EC_PUBLIC_KEY); @@ -211,4 +222,14 @@ void deriveAddress() { assertThat(ED_PUBLIC_KEY.deriveAddress().value()).isEqualTo("rwGWYtRR6jJJJq7FKQg74YwtkiPyUqJ466"); assertThat(EC_PUBLIC_KEY.deriveAddress().value()).isEqualTo("rD8ATvjj9mfnFuYYTGRNb9DygnJW9JNN1C"); } + + /////////////////// + // Constants + /////////////////// + + @Test + void testConstants() { + assertThat(PublicKey.ED2559_PREFIX.asInt()).isEqualTo(0xED); + assertThat(PublicKey.ED2559_PREFIX.asByte()).isEqualTo((byte) 0xED); + } } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/SeedTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/SeedTest.java index b59c0bebd..4e4777415 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/SeedTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/SeedTest.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. @@ -29,24 +29,19 @@ import org.xrpl.xrpl4j.codec.addresses.KeyType; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.codec.addresses.exceptions.DecodeException; -import org.xrpl.xrpl4j.crypto.keys.Base58EncodedSecret; -import org.xrpl.xrpl4j.crypto.keys.Entropy; -import org.xrpl.xrpl4j.crypto.keys.KeyPair; -import org.xrpl.xrpl4j.crypto.keys.Passphrase; -import org.xrpl.xrpl4j.crypto.keys.PrivateKey; -import org.xrpl.xrpl4j.crypto.keys.PublicKey; -import org.xrpl.xrpl4j.crypto.keys.Seed; import org.xrpl.xrpl4j.crypto.keys.Seed.DefaultSeed; +import java.math.BigInteger; import javax.security.auth.DestroyFailedException; /** * Unit tests for {@link Seed}. */ +@SuppressWarnings("deprecation") public class SeedTest { - private Seed edSeed = Seed.ed25519SeedFromPassphrase(Passphrase.of("hello")); - private Seed ecSeed = Seed.secp256k1SeedFromPassphrase(Passphrase.of("hello")); + private final Seed edSeed = Seed.ed25519SeedFromPassphrase(Passphrase.of("hello")); + private final Seed ecSeed = Seed.secp256k1SeedFromPassphrase(Passphrase.of("hello")); @Test void constructorWithNullSeed() { @@ -58,10 +53,7 @@ void constructorWithNullSeed() { @Test void constructorWithNullUnsignedByteArray() { - assertThrows(NullPointerException.class, () -> { - UnsignedByteArray nullUba = null; - new DefaultSeed(nullUba); - }); + assertThrows(NullPointerException.class, () -> new DefaultSeed((UnsignedByteArray) null)); } @Test @@ -126,6 +118,7 @@ void testSecp256k1SeedFromPassphraseWithNull() { @Test public void testEd25519SeedFromPassphrase() throws DestroyFailedException { + //noinspection OptionalGetWithoutIsPresent assertThat(edSeed.decodedSeed().type().get()).isEqualTo(KeyType.ED25519); assertThat(BaseEncoding.base64().encode(edSeed.decodedSeed().bytes().toByteArray())) .isEqualTo("m3HSJL1i83hdltRq0+o9cw=="); @@ -136,6 +129,7 @@ public void testEd25519SeedFromPassphrase() throws DestroyFailedException { @Test public void testSecp256k1SeedFromPassphrase() throws DestroyFailedException { + //noinspection OptionalGetWithoutIsPresent assertThat(ecSeed.decodedSeed().type().get()).isEqualTo(KeyType.SECP256K1); assertThat(BaseEncoding.base64().encode(ecSeed.decodedSeed().bytes().toByteArray())) .isEqualTo("m3HSJL1i83hdltRq0+o9cw=="); @@ -163,10 +157,110 @@ void seedFromBase58EncodedSecretSecp256k1() { assertThat(seed.decodedSeed().bytes().hexValue()).isEqualTo("0102030405060708090A0B0C0D0E0F10"); } + /** + * Verify {@link Seed#deriveKeyPair()} using a seed that would have produced a 32 byte private key prior to fixing + * #486. + * + * @see "https://github.com/XRPLF/xrpl4j/issues/486" + */ + @Test + void deriveKeyPairFor32ByteEcSeed() { + // Without explicit padding added by xrpl4j, but instead due to twos-complement padding, this seed would have + // (prior to fixing #486) produced a 32 byte private key. We validate here that it produces the correct output + // for either value() call. + final Seed ecSeedFor32BytePrivateKey = Seed.secp256k1SeedFromEntropy( + Entropy.of(BaseEncoding.base16().decode("B7E99C6BB9786494238D2BA84EABE854")) + ); + final PrivateKey privateKey = ecSeedFor32BytePrivateKey.deriveKeyPair().privateKey(); + assertThat(privateKey.value().hexValue()) // <- Overtly test .value() + .isEqualTo("007030CBD40D6961E625AD73159A4B463AA42B4E88CC2248AC49E1EDCB50AF2924"); + assertThat(privateKey.prefixedBytes().hexValue()) + .isEqualTo("007030CBD40D6961E625AD73159A4B463AA42B4E88CC2248AC49E1EDCB50AF2924"); + assertThat(privateKey.naturalBytes().hexValue()) + .isEqualTo("7030CBD40D6961E625AD73159A4B463AA42B4E88CC2248AC49E1EDCB50AF2924"); + } + + /** + * Verify {@link Seed#deriveKeyPair()} using a seed that would have produced a 33 byte private key prior to fixing + * #486. + * + * @see "https://github.com/XRPLF/xrpl4j/issues/486" + */ + @Test + void deriveKeyPairFor33ByteEcSeed() { + // Without explicit padding added by xrpl4j, but instead due to twos-complement padding, this seed would have + // (prior to fixing #486) produced a 33 byte private key. We validate here that it produces the correct output + // for either value() call. + final Seed ecSeedFor32BytePrivateKey = Seed.secp256k1SeedFromEntropy( + Entropy.of(BaseEncoding.base16().decode("358C75D5AD5E2FF5E19256978F18F0B9")) + ); + final PrivateKey privateKey = ecSeedFor32BytePrivateKey.deriveKeyPair().privateKey(); + assertThat(privateKey.value().hexValue()) // <- Overtly test .value() + .isEqualTo("00FBD60C9C99BA3D4706A449C30E4D61DCC3811E23EF69291F98A886CEC6A8B0B5"); + assertThat(privateKey.prefixedBytes().hexValue()) + .isEqualTo("00FBD60C9C99BA3D4706A449C30E4D61DCC3811E23EF69291F98A886CEC6A8B0B5"); + assertThat(privateKey.naturalBytes().hexValue()) + .isEqualTo("FBD60C9C99BA3D4706A449C30E4D61DCC3811E23EF69291F98A886CEC6A8B0B5"); + } + + /** + * Verify that BigInteger construction behaves the same no matter how many zero-byte prefix pads are used. + */ + @Test + void verifyBigIntegerConstructionForSecp256k1PrivateKeys() { + // This seed will generate a secp256k1 SecretKey D value of + // 53AC3F62A5A6E598C7D1E31AB92587C56823A1BE5C21E53ABE9D9A722E5236 (which is only 31 bytes long). + Seed ecSeedFor31BytePrivateKey = Seed.fromBase58EncodedSecret( + Base58EncodedSecret.of("shZrqKhyGi5Gyvrv5AJawNMuR2WaN") + ); + + assertThat(BaseEncoding.base16().decode("53AC3F62A5A6E598C7D1E31AB92587C56823A1BE5C21E53ABE9D9A722E5236").length) + .isEqualTo(31); + assertThat(ecSeedFor31BytePrivateKey.deriveKeyPair().privateKey().naturalBytes().toByteArray().length) + .isEqualTo(32); + assertThat(ecSeedFor31BytePrivateKey.deriveKeyPair().privateKey().prefixedBytes().toByteArray().length) + .isEqualTo(33); + + // Assert that all BigIntegers are equivalent (i.e., that the constructor ignores zero-byte pads) + BigInteger shortBigInt = new BigInteger( + BaseEncoding.base16().decode("53AC3F62A5A6E598C7D1E31AB92587C56823A1BE5C21E53ABE9D9A722E5236") + ); + BigInteger unPaddedBitInt = new BigInteger( + 1, ecSeedFor31BytePrivateKey.deriveKeyPair().privateKey().naturalBytes().toByteArray() + ); + BigInteger paddedBitInt = new BigInteger( + 1, ecSeedFor31BytePrivateKey.deriveKeyPair().privateKey().prefixedBytes().toByteArray() + ); + + assertThat(shortBigInt).isEqualTo(unPaddedBitInt); + assertThat(shortBigInt).isEqualTo(paddedBitInt); + assertThat(paddedBitInt).isEqualTo(unPaddedBitInt); + } + + /** + * Verify {@link Seed#deriveKeyPair()} using a seed that would have produced a 31 byte private key (prior to fixing + * #486) generates expected results. + * + * @see "https://github.com/XRPLF/xrpl4j/issues/486" + */ + @Test + void deriveKeyPairFor31ByteEcSeed() { + Seed ecSeedFor31BytePrivateKey = Seed.fromBase58EncodedSecret( + Base58EncodedSecret.of("shZrqKhyGi5Gyvrv5AJawNMuR2WaN") + ); + final PrivateKey privateKey = ecSeedFor31BytePrivateKey.deriveKeyPair().privateKey(); + assertThat(privateKey.value().hexValue()) // <- Overtly test .value() + .isEqualTo("000053AC3F62A5A6E598C7D1E31AB92587C56823A1BE5C21E53ABE9D9A722E5236"); + assertThat(privateKey.prefixedBytes().hexValue()) + .isEqualTo("000053AC3F62A5A6E598C7D1E31AB92587C56823A1BE5C21E53ABE9D9A722E5236"); + assertThat(privateKey.naturalBytes().hexValue()) + .isEqualTo("0053AC3F62A5A6E598C7D1E31AB92587C56823A1BE5C21E53ABE9D9A722E5236"); + } + @Test void testEquals() { - assertThat(edSeed).isEqualTo(edSeed); - assertThat(ecSeed).isEqualTo(ecSeed); + assertThat(edSeed.equals(edSeed)).isTrue(); + assertThat(ecSeed.equals(ecSeed)).isTrue(); assertThat(edSeed).isNotEqualTo(ecSeed); assertThat(ecSeed).isNotEqualTo(edSeed); assertThat(ecSeed).isNotEqualTo(new Object()); @@ -200,7 +294,7 @@ public void deriveEd25519KeyPair() { KeyPair keyPair = Seed.DefaultSeed.Ed25519KeyPairService.deriveKeyPair(seed); KeyPair expectedKeyPair = KeyPair.builder() - .privateKey(PrivateKey.of(UnsignedByteArray.of( + .privateKey(PrivateKey.fromPrefixedBytes(UnsignedByteArray.of( BaseEncoding.base16().decode("ED2F1185B6F5525D7A7D2A22C1D8BAEEBEEFFE597C9010AF916EBB9447BECC5BE6" )))) .publicKey( @@ -232,7 +326,7 @@ public void deriveSecp256k1KeyPair() { Seed seed = Seed.fromBase58EncodedSecret(Base58EncodedSecret.of("sp5fghtJtpUorTwvof1NpDXAzNwf5")); KeyPair keyPair = Seed.DefaultSeed.Secp256k1KeyPairService.deriveKeyPair(seed); KeyPair expectedKeyPair = KeyPair.builder() - .privateKey(PrivateKey.of(UnsignedByteArray.of( + .privateKey(PrivateKey.fromPrefixedBytes(UnsignedByteArray.of( BaseEncoding.base16().decode("00D78B9735C3F26501C7337B8A5727FD53A6EFDBC6AA55984F098488561F985E23" )))) .publicKey( @@ -249,8 +343,9 @@ public void generateSeedFromEd25519Seed() { Seed seed = Seed.ed25519SeedFromEntropy(entropy); assertThat(seed.deriveKeyPair().publicKey()).isEqualTo( PublicKey.fromBase16EncodedPublicKey("ED01FA53FA5A7E77798F882ECE20B1ABC00BB358A9E55A202D0D0676BD0CE37A63")); - assertThat(seed.deriveKeyPair().privateKey()).isEqualTo( - PrivateKey.of(UnsignedByteArray.fromHex("EDB4C4E046826BD26190D09715FC31F4E6A728204EADD112905B08B14B7F15C4F3"))); + assertThat(seed.deriveKeyPair().privateKey()).isEqualTo(PrivateKey.fromPrefixedBytes( + UnsignedByteArray.fromHex("EDB4C4E046826BD26190D09715FC31F4E6A728204EADD112905B08B14B7F15C4F3") + )); assertThat(seed.deriveKeyPair().publicKey().deriveAddress().value()).isEqualTo( "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD"); } @@ -260,10 +355,13 @@ public void generateWalletFromSecp256k1Seed() { Entropy entropy = Entropy.of(BaseEncoding.base16().decode("CC4E55BC556DD561CBE990E3D4EF7069")); Seed seed = Seed.secp256k1SeedFromEntropy(entropy); assertThat(seed.deriveKeyPair().publicKey().base16Value()).isEqualTo( - "02FD0E8479CE8182ABD35157BB0FA17A469AF27DCB12B5DDED697C61809116A33B"); - assertThat(seed.deriveKeyPair().privateKey().value().hexValue()).isEqualTo( - "27690792130FC12883E83AE85946B018B3BEDE6EEDCDA3452787A94FC0A17438"); + "02FD0E8479CE8182ABD35157BB0FA17A469AF27DCB12B5DDED697C61809116A33B" + ); + assertThat(seed.deriveKeyPair().privateKey().prefixedBytes().hexValue()).isEqualTo( + "0027690792130FC12883E83AE85946B018B3BEDE6EEDCDA3452787A94FC0A17438" + ); assertThat(seed.deriveKeyPair().publicKey().deriveAddress().value()).isEqualTo( - "rByLcEZ7iwTBAK8FfjtpFuT7fCzt4kF4r2"); + "rByLcEZ7iwTBAK8FfjtpFuT7fCzt4kF4r2" + ); } } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/bc/BcKeyUtilsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/bc/BcKeyUtilsTest.java index 561287d17..1dbcc3a96 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/bc/BcKeyUtilsTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/bc/BcKeyUtilsTest.java @@ -56,9 +56,10 @@ void edPrivateKeyParametersToPrivateKeyAndBack() { // To PrivateKey PrivateKey privateKey = BcKeyUtils.toPrivateKey(ed25519PrivateKeyParameters); - assertThat(Base58.encode(privateKey.value().toByteArray())) + assertThat(Base58.encode(privateKey.prefixedBytes().toByteArray())) .isEqualTo("pDcQTi2uFBAzQ7cY2mYQtk9QuQBoLU6rJypEf8EYPQoouh"); - assertThat(BaseEncoding.base16().encode(privateKey.value().toByteArray())).isEqualTo("ED" + ED_PRIVATE_KEY_HEX); + assertThat(BaseEncoding.base16().encode(privateKey.prefixedBytes().toByteArray())) + .isEqualTo("ED" + ED_PRIVATE_KEY_HEX); // Convert back Ed25519PrivateKeyParameters converted = BcKeyUtils.toEd25519PrivateKeyParams(privateKey); @@ -73,10 +74,10 @@ void ecPrivateKeyParametersToPrivateKeyAndBack() { // To PrivateKey PrivateKey privateKey = BcKeyUtils.toPrivateKey(ecPrivateKeyParameters); - assertThat(Base58.encode(privateKey.value().toByteArray())) - .isEqualTo("EnYwxojogCYKG3F5Bf7zvcZjo76pEqKwG9wGH14JngcV"); - assertThat(BaseEncoding.base16().encode(privateKey.value().toByteArray())) - .isEqualTo("D12D2FACA9AD92828D89683778CB8DFCCDBD6C9E92F6AB7D6065E8AACC1FF6D6"); + assertThat(Base58.encode(privateKey.prefixedBytes().toByteArray())) + .isEqualTo("rEnYwxojogCYKG3F5Bf7zvcZjo76pEqKwG9wGH14JngcV"); + assertThat(BaseEncoding.base16().encode(privateKey.prefixedBytes().toByteArray())) + .isEqualTo("00D12D2FACA9AD92828D89683778CB8DFCCDBD6C9E92F6AB7D6065E8AACC1FF6D6"); assertThat(BcKeyUtils.toEcPrivateKeyParams(privateKey)).usingRecursiveComparison() .isEqualTo(ecPrivateKeyParameters); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureServiceTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureServiceTest.java index 6abab24a9..03abb1692 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureServiceTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureServiceTest.java @@ -193,12 +193,12 @@ public void signWithNullPrivateKey() { @Test public void signWithNullTransaction() { assertThrows(NullPointerException.class, - () -> signatureService.sign(TestConstants.ED_PRIVATE_KEY, (Transaction) null)); + () -> signatureService.sign(TestConstants.getEdPrivateKey(), (Transaction) null)); } @Test public void signEd25519() { - signatureService.sign(TestConstants.ED_PRIVATE_KEY, transactionMock); + signatureService.sign(TestConstants.getEdPrivateKey(), transactionMock); verify(signatureUtilsMock, times(0)).toMultiSignableBytes(any(), any()); verify(signatureUtilsMock).toSignableBytes(transactionMock); @@ -208,7 +208,7 @@ public void signEd25519() { @Test public void signSecp256k1() { - signatureService.sign(TestConstants.EC_PRIVATE_KEY, transactionMock); + signatureService.sign(TestConstants.getEcPrivateKey(), transactionMock); verify(signatureUtilsMock, times(0)).toMultiSignableBytes(any(), any()); verify(signatureUtilsMock).toSignableBytes(transactionMock); @@ -220,7 +220,7 @@ public void signSecp256k1() { void multiSignEd25519() { when(signedTransactionMock.signature()).thenReturn(ed25519SignatureMock); - final Signature signature = signatureService.multiSign(TestConstants.ED_PRIVATE_KEY, transactionMock); + final Signature signature = signatureService.multiSign(TestConstants.getEdPrivateKey(), transactionMock); assertThat(signature).isEqualTo(ed25519SignatureMock); verify(signatureUtilsMock).toMultiSignableBytes(transactionMock, TestConstants.ED_ADDRESS); @@ -232,7 +232,7 @@ void multiSignEd25519() { void multiSignSecp256k1() { when(signedTransactionMock.signature()).thenReturn(secp256k1SignatureMock); - final Signature signature = signatureService.multiSign(TestConstants.EC_PRIVATE_KEY, transactionMock); + final Signature signature = signatureService.multiSign(TestConstants.getEcPrivateKey(), transactionMock); assertThat(signature).isEqualTo(secp256k1SignatureMock); verify(signatureUtilsMock).toMultiSignableBytes(transactionMock, TestConstants.EC_ADDRESS); @@ -252,7 +252,7 @@ public void signUnsignedClaimWithNullPrivateKey() { @Test public void signUnsignedClaimWithNullUnsignedClaim() { assertThrows(NullPointerException.class, - () -> signatureService.sign(TestConstants.ED_PRIVATE_KEY, (UnsignedClaim) null)); + () -> signatureService.sign(TestConstants.getEdPrivateKey(), (UnsignedClaim) null)); } @Test @@ -260,7 +260,7 @@ public void signUnsignedClaimEd25519() { UnsignedClaim unsignedClaimMock = mock(UnsignedClaim.class); when(signatureUtilsMock.toSignableBytes(unsignedClaimMock)).thenReturn(UnsignedByteArray.empty()); - Signature actualSignature = signatureService.sign(TestConstants.ED_PRIVATE_KEY, unsignedClaimMock); + Signature actualSignature = signatureService.sign(TestConstants.getEdPrivateKey(), unsignedClaimMock); assertThat(actualSignature).isEqualTo(ed25519SignatureMock); verify(signatureUtilsMock, times(0)).toMultiSignableBytes(any(), any()); @@ -350,7 +350,7 @@ public void verifyMultiSecp256k1() { @Test public void edDsaSign() { - Signature actual = signatureService.edDsaSign(TestConstants.ED_PRIVATE_KEY, UnsignedByteArray.empty()); + Signature actual = signatureService.edDsaSign(TestConstants.getEdPrivateKey(), UnsignedByteArray.empty()); assertThat(actual).isEqualTo(ed25519SignatureMock); assertThat(ed25519VerifyCalled.get()).isFalse(); @@ -364,7 +364,7 @@ public void edDsaSign() { @Test public void ecDsaSign() { - Signature actual = signatureService.ecDsaSign(TestConstants.EC_PRIVATE_KEY, UnsignedByteArray.empty()); + Signature actual = signatureService.ecDsaSign(TestConstants.getEcPrivateKey(), UnsignedByteArray.empty()); assertThat(actual).isEqualTo(secp256k1SignatureMock); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java index 0590a68c1..bd0441ff1 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java @@ -47,12 +47,25 @@ import org.xrpl.xrpl4j.codec.binary.XrplBinaryCodec; import org.xrpl.xrpl4j.crypto.keys.PublicKey; import org.xrpl.xrpl4j.model.client.channels.UnsignedClaim; +import org.xrpl.xrpl4j.model.flags.AmmDepositFlags; +import org.xrpl.xrpl4j.model.flags.AmmWithdrawFlags; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; +import org.xrpl.xrpl4j.model.ledger.AuthAccount; +import org.xrpl.xrpl4j.model.ledger.AuthAccountWrapper; +import org.xrpl.xrpl4j.model.ledger.Issue; import org.xrpl.xrpl4j.model.transactions.AccountDelete; import org.xrpl.xrpl4j.model.transactions.AccountSet; import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.AmmBid; +import org.xrpl.xrpl4j.model.transactions.AmmCreate; +import org.xrpl.xrpl4j.model.transactions.AmmDelete; +import org.xrpl.xrpl4j.model.transactions.AmmDeposit; +import org.xrpl.xrpl4j.model.transactions.AmmVote; +import org.xrpl.xrpl4j.model.transactions.AmmWithdraw; import org.xrpl.xrpl4j.model.transactions.CheckCancel; import org.xrpl.xrpl4j.model.transactions.CheckCash; import org.xrpl.xrpl4j.model.transactions.CheckCreate; +import org.xrpl.xrpl4j.model.transactions.Clawback; import org.xrpl.xrpl4j.model.transactions.DepositPreAuth; import org.xrpl.xrpl4j.model.transactions.EscrowCancel; import org.xrpl.xrpl4j.model.transactions.EscrowCreate; @@ -75,6 +88,7 @@ import org.xrpl.xrpl4j.model.transactions.SignerListSet; import org.xrpl.xrpl4j.model.transactions.SignerWrapper; import org.xrpl.xrpl4j.model.transactions.TicketCreate; +import org.xrpl.xrpl4j.model.transactions.TradingFee; import org.xrpl.xrpl4j.model.transactions.Transaction; import org.xrpl.xrpl4j.model.transactions.TrustSet; import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; @@ -573,6 +587,154 @@ void addSignatureToTicketCreate() { addSignatureToTransactionHelper(ticketCreate); } + @Test + void addSignatureToAmmBid() { + AmmBid bid = AmmBid.builder() + .account(sourcePublicKey.deriveAddress()) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .addAuthAccounts( + AuthAccountWrapper.of(AuthAccount.of(Address.of("rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg"))), + AuthAccountWrapper.of(AuthAccount.of(Address.of("rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv"))) + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(9)) + .signingPublicKey(sourcePublicKey) + .build(); + + addSignatureToTransactionHelper(bid); + } + + @Test + void addSignatureToAmmCreate() { + AmmCreate ammCreate = AmmCreate.builder() + .account(sourcePublicKey.deriveAddress()) + .amount( + IssuedCurrencyAmount.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .value("25") + .build() + ) + .amount2(XrpCurrencyAmount.ofDrops(250000000)) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(6)) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(500))) + .signingPublicKey(sourcePublicKey) + .build(); + + addSignatureToTransactionHelper(ammCreate); + } + + @Test + void addSignatureToAmmDeposit() { + AmmDeposit deposit = AmmDeposit.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .flags(AmmDepositFlags.LIMIT_LP_TOKEN) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .lpTokenOut( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ) + .signingPublicKey(sourcePublicKey) + .build(); + + addSignatureToTransactionHelper(deposit); + } + + @Test + void addSignatureToAmmVote() { + AmmVote vote = AmmVote.builder() + .account(sourcePublicKey.deriveAddress()) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .build() + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(8)) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(600))) + .signingPublicKey(sourcePublicKey) + .build(); + + addSignatureToTransactionHelper(vote); + } + + @Test + void addSignatureToAmmWithdraw() { + AmmWithdraw withdraw = AmmWithdraw.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .asset( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .asset2(Issue.XRP) + .flags(AmmWithdrawFlags.WITHDRAW_ALL) + .signingPublicKey(sourcePublicKey) + .build(); + + addSignatureToTransactionHelper(withdraw); + } + + @Test + void addSignatureToAmmDelete() { + AmmDelete ammDelete = AmmDelete.builder() + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(9)) + .flags(TransactionFlags.UNSET) + .signingPublicKey(sourcePublicKey) + .build(); + + addSignatureToTransactionHelper(ammDelete); + } + + @Test + void addSignatureToClawback() { + Clawback clawback = Clawback.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.ONE) + .signingPublicKey(sourcePublicKey) + .amount( + IssuedCurrencyAmount.builder() + .currency("FOO") + .issuer(Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW")) + .value("314.159") + .build() + ) + .build(); + + addSignatureToTransactionHelper(clawback); + } + @Test public void addSignatureToTransactionUnsupported() { assertThrows(IllegalArgumentException.class, () -> addSignatureToTransactionHelper(transactionMock)); @@ -892,6 +1054,147 @@ void addMultiSignaturesToTicketCreate() { addMultiSignatureToTransactionHelper(ticketCreate); } + @Test + void addMultiSignaturesToClawback() { + Clawback clawback = Clawback.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.ONE) + .amount( + IssuedCurrencyAmount.builder() + .currency("FOO") + .issuer(Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW")) + .value("314.159") + .build() + ) + .build(); + + addMultiSignatureToTransactionHelper(clawback); + } + + @Test + void addMultiSignatureToAmmBid() { + AmmBid bid = AmmBid.builder() + .account(sourcePublicKey.deriveAddress()) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .addAuthAccounts( + AuthAccountWrapper.of(AuthAccount.of(Address.of("rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg"))), + AuthAccountWrapper.of(AuthAccount.of(Address.of("rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv"))) + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(9)) + .build(); + + addMultiSignatureToTransactionHelper(bid); + } + + @Test + void addMultiSignatureToAmmCreate() { + AmmCreate ammCreate = AmmCreate.builder() + .account(sourcePublicKey.deriveAddress()) + .amount( + IssuedCurrencyAmount.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .value("25") + .build() + ) + .amount2(XrpCurrencyAmount.ofDrops(250000000)) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(6)) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(500))) + .build(); + + addMultiSignatureToTransactionHelper(ammCreate); + } + + @Test + void addMultiSignatureToAmmDeposit() { + AmmDeposit deposit = AmmDeposit.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .flags(AmmDepositFlags.LIMIT_LP_TOKEN) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .lpTokenOut( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ) + .build(); + + addMultiSignatureToTransactionHelper(deposit); + } + + @Test + void addMultiSignatureToAmmVote() { + AmmVote vote = AmmVote.builder() + .account(sourcePublicKey.deriveAddress()) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .build() + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(8)) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(600))) + .build(); + + addMultiSignatureToTransactionHelper(vote); + } + + @Test + void addMultiSignatureToAmmWithdraw() { + AmmWithdraw withdraw = AmmWithdraw.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .asset( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .asset2(Issue.XRP) + .flags(AmmWithdrawFlags.WITHDRAW_ALL) + .build(); + + addMultiSignatureToTransactionHelper(withdraw); + } + + @Test + void addMultiSignatureToAmmDelete() { + AmmDelete ammDelete = AmmDelete.builder() + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(9)) + .flags(TransactionFlags.UNSET) + .build(); + + addMultiSignatureToTransactionHelper(ammDelete); + } + @Test public void addMultiSignaturesToTransactionUnsupported() { when(transactionMock.transactionSignature()).thenReturn(Optional.empty()); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/bc/Secp256k1Test.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/bc/Secp256k1Test.java new file mode 100644 index 000000000..67c44446a --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/bc/Secp256k1Test.java @@ -0,0 +1,277 @@ +package org.xrpl.xrpl4j.crypto.signing.bc; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.common.io.BaseEncoding; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.xrpl.xrpl4j.codec.addresses.UnsignedByte; +import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; + +import java.math.BigInteger; +import java.util.Locale; +import java.util.stream.Stream; + +/** + * Unit tests for {@link Secp256k1}. + */ +class Secp256k1Test { + + ///////// + // toUnsignedByteArray(BigInteger) + ///////// + + @Test + void fromBigIntegerWithInvalidInputs() { + assertThrows(NullPointerException.class, () -> Secp256k1.toUnsignedByteArray( + null, // <-- The crux of the test + 0 + )); + assertThrows(IllegalArgumentException.class, () -> Secp256k1.toUnsignedByteArray( + BigInteger.valueOf(-1L), // <-- The crux of the test + 0 + )); + assertThrows(IllegalArgumentException.class, () -> Secp256k1.toUnsignedByteArray( + BigInteger.valueOf(1L), + -1 // <-- The crux of the test + )); + } + + @Test + void fromBigIntegerWithZeroLength() { + Assertions.assertThat(Secp256k1.toUnsignedByteArray( + BigInteger.valueOf(1L), + 0 // <-- The crux of the test + ) + .hexValue()).isEqualTo("01"); + } + + @ParameterizedTest + @ArgumentsSource(BigIntegerByteEncodingsProvider.class) + void fromBigIntegerWithZeroPaddingBytes( + final String amountString, + final String amountToString16, + final String amountToByteArrayHexUnpadded, + final String amountToByteArrayHexPrefixPadded + ) { + final BigInteger amount = new BigInteger(amountString); + // NOTE `amount.toString(16)` strips off all leading 0s, even in a nibble (beware of using this in actual impl code) + Assertions.assertThat(amount.toString(16).toUpperCase(Locale.ENGLISH)).isEqualTo(amountToString16); + Assertions.assertThat(BaseEncoding.base16().encode(amount.toByteArray())).isEqualTo(amountToByteArrayHexUnpadded); + Assertions.assertThat(Secp256k1.toUnsignedByteArray(amount, 33).hexValue()) + .isEqualTo(amountToByteArrayHexPrefixPadded); + } + + @Test + void fromBigIntegerWithNumberGreaterThan33Bytes() { + final BigInteger amount = new BigInteger( + "194815934319126504488398097255143744553440248783815166056530734282223472643" + + "194815934319126504488398097255143744553440248783815166056530734282223472643"); + // NOTE `amount.toString(16)` strips off all leading 0s, even in a nibble (beware of using this in actual impl code) + Assertions.assertThat(amount.toString(16).toUpperCase(Locale.ENGLISH)).isEqualTo( + "F3C607BB6CA7C4C335A24D0302484D16956259AC4510289E3E77A87BD72F36D1EED47F97D33F05F1715F603B45E83748DE37C087" + + "9DDE6060821AAAAD5003" + ); + Assertions.assertThat(BaseEncoding.base16().encode(amount.toByteArray())).isEqualTo( + "00F3C607BB6CA7C4C335A24D0302484D16956259AC4510289E3E77A87BD72F36D1EED47F97D33F05F1715F603B45E83748DE37C087" + + "9DDE6060821AAAAD5003"); + Assertions.assertThat(Secp256k1.toUnsignedByteArray(amount, 33).hexValue()).isEqualTo( + "00F3C607BB6CA7C4C335A24D0302484D16956259AC4510289E3E77A87BD72F36D1EED47F97D33F05F1715F603B45E83748DE37C087" + + "9DDE6060821AAAAD5003"); + } + + ///////////////////////// + // withZeroPrefixPadding(UnsignedByteArray) + ///////////////////////// + + @Test + void withZeroPrefixPaddingWithUnsignedByteArrayWithInvalidInputs() { + UnsignedByteArray nullUba = null; + assertThrows(NullPointerException.class, () -> Secp256k1.withZeroPrefixPadding( + nullUba, // <-- The crux of the test + 0 + )); + + assertThat(Secp256k1.withZeroPrefixPadding( + UnsignedByteArray.of(BigInteger.valueOf(-1L).toByteArray()), // <-- The crux of the test + 0 + )).isEqualTo(UnsignedByteArray.of(UnsignedByte.of(255))); + + assertThrows(IllegalArgumentException.class, () -> Secp256k1.withZeroPrefixPadding( + UnsignedByteArray.of(BigInteger.valueOf(1L).toByteArray()), + -1 // <-- The crux of the test + )); + } + + @Test + void withZeroPrefixPaddingWithUnsignedByteArrayWithZeroLength() { + Assertions.assertThat(Secp256k1.withZeroPrefixPadding( + UnsignedByteArray.of(BigInteger.valueOf(1L).toByteArray()), + 0 // <-- The crux of the test + ) + .hexValue()).isEqualTo("01"); + } + + @ParameterizedTest + @ArgumentsSource(BigIntegerByteEncodingsProvider.class) + void withZeroPrefixPaddingWithUnsignedByteArray( + final String amountString, + final String amountToString16, + final String amountToByteArrayHexUnpadded, + final String amountToByteArrayHexPrefixPadded + ) { + final BigInteger amount = new BigInteger(amountString); + // NOTE `amount.toString(16)` strips off all leading 0s, even in a nibble (beware of using this in actual impl code) + Assertions.assertThat(amount.toString(16).toUpperCase(Locale.ENGLISH)).isEqualTo(amountToString16); + Assertions.assertThat(BaseEncoding.base16().encode(amount.toByteArray())).isEqualTo(amountToByteArrayHexUnpadded); + + UnsignedByteArray uba = UnsignedByteArray.of(amount.toByteArray()); + Assertions.assertThat(Secp256k1.withZeroPrefixPadding(uba, 33).hexValue()) + .isEqualTo(amountToByteArrayHexPrefixPadded); + } + + @Test + void withZeroPrefixPaddingWithUnsignedByteArrayExtend32() { + final byte[] bytes32 = new byte[32]; + final UnsignedByteArray uba32 = UnsignedByteArray.of(bytes32); + final byte[] bytes33 = new byte[33]; + final UnsignedByteArray uba33 = UnsignedByteArray.of(bytes33); + + assertThrows(IllegalArgumentException.class, () -> Secp256k1.withZeroPrefixPadding(uba32, -1)); + assertThat(Secp256k1.withZeroPrefixPadding(uba32, 0)).isEqualTo(uba32); + assertThat(Secp256k1.withZeroPrefixPadding(uba32, 1)).isEqualTo(uba32); + assertThat(Secp256k1.withZeroPrefixPadding(uba32, 32)).isEqualTo(uba32); + assertThat(Secp256k1.withZeroPrefixPadding(uba32, 33)).isEqualTo(uba33); + } + + ///////////////////////// + // withZeroPrefixPadding(byte[]) + ///////////////////////// + + @Test + void withZeroPrefixPaddingWithByteArrayWithInvalidInputs() { + byte[] nullByteArray = null; + assertThrows(NullPointerException.class, () -> Secp256k1.withZeroPrefixPadding( + nullByteArray, // <-- The crux of the test + 0 + )); + + assertThat(Secp256k1.withZeroPrefixPadding( + BigInteger.valueOf(-1L).toByteArray(), // <-- The crux of the test + 0 + )).isEqualTo(UnsignedByteArray.of(UnsignedByte.of(255))); + + assertThrows(IllegalArgumentException.class, () -> Secp256k1.withZeroPrefixPadding( + BigInteger.valueOf(1L).toByteArray(), + -1 // <-- The crux of the test + )); + } + + @Test + void withZeroPrefixPaddingWithByteArrayWithZeroLength() { + Assertions.assertThat(Secp256k1.withZeroPrefixPadding( + BigInteger.valueOf(1L).toByteArray(), + 0 // <-- The crux of the test + ) + .hexValue()).isEqualTo("01"); + } + + @ParameterizedTest + @ArgumentsSource(BigIntegerByteEncodingsProvider.class) + void withZeroPrefixPaddingWithByteArray( + final String amountString, + final String amountToString16, + final String amountToByteArrayHexUnpadded, + final String amountToByteArrayHexPrefixPadded + ) { + final BigInteger amount = new BigInteger(amountString); + // NOTE `amount.toString(16)` strips off all leading 0s, even in a nibble (beware of using this in actual impl code) + Assertions.assertThat(amount.toString(16).toUpperCase(Locale.ENGLISH)).isEqualTo(amountToString16); + Assertions.assertThat(BaseEncoding.base16().encode(amount.toByteArray())).isEqualTo(amountToByteArrayHexUnpadded); + Assertions.assertThat(Secp256k1.withZeroPrefixPadding(amount.toByteArray(), 33).hexValue()) + .isEqualTo(amountToByteArrayHexPrefixPadded); + } + + @Test + void withZeroPrefixPaddingWithByteArrayExtend32() { + final byte[] bytes32 = new byte[32]; + final UnsignedByteArray uba32 = UnsignedByteArray.of(bytes32); + final byte[] bytes33 = new byte[33]; + final UnsignedByteArray uba33 = UnsignedByteArray.of(bytes33); + + assertThrows(IllegalArgumentException.class, () -> Secp256k1.withZeroPrefixPadding(bytes32, -1)); + assertThat(Secp256k1.withZeroPrefixPadding(bytes32, 0)).isEqualTo(uba32); + assertThat(Secp256k1.withZeroPrefixPadding(bytes32, 1)).isEqualTo(uba32); + assertThat(Secp256k1.withZeroPrefixPadding(bytes32, 32)).isEqualTo(uba32); + assertThat(Secp256k1.withZeroPrefixPadding(bytes32, 33)).isEqualTo(uba33); + } + + /** + * An {@link ArgumentsProvider} that provides expected binary encodings for a variety of BigInteger representations. + */ + static class BigIntegerByteEncodingsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + // A BigInteger comprised of 33 Bytes; toByteArray has 33 bytes; 0 extra padding added + Arguments.of( + "107371972967791294617431936514364612285184717182742299921102689634201232605691", // BigInt + "ED6262116F8D51F1FDD98C184F74CA48DDA7B049CB741F1F7A0564B88FE601FB", // .toString(16) + "00ED6262116F8D51F1FDD98C184F74CA48DDA7B049CB741F1F7A0564B88FE601FB", // .toByteArray() + "00ED6262116F8D51F1FDD98C184F74CA48DDA7B049CB741F1F7A0564B88FE601FB" // <-- Padded to 33 bytes + ), + // A BigInteger comprised of 33 Bytes; toByteArray has 33 bytes; 0 extra padding added + Arguments.of( + "84513109120471239583994879976286018548016554258021069224677925571161262209437", // BigInt + "BAD8B981A239980B1EC4CB901D698DDE7AA15F264D9537C7D141EE119DD5399D", // .toString(16) + "00BAD8B981A239980B1EC4CB901D698DDE7AA15F264D9537C7D141EE119DD5399D", // .toByteArray() + "00BAD8B981A239980B1EC4CB901D698DDE7AA15F264D9537C7D141EE119DD5399D" // <-- BigInteger Hex, Padded to 33 bytes + ), + // A BigInteger comprised of 32 Bytes; toByteArray has 32 bytes; 1 extra padding added + Arguments.of( + "8427551091932113544047724072139537481003293113704693219824523888925672289487", // BigInt + "12A1D32B744B18FA0186A44F32D9241869FA0A05B5B831F188831A07163534CF", // .toString(16) + "12A1D32B744B18FA0186A44F32D9241869FA0A05B5B831F188831A07163534CF", // .toByteArray() + "0012A1D32B744B18FA0186A44F32D9241869FA0A05B5B831F188831A07163534CF"// <-- BigInteger Hex, Padded to 33 bytes + ), + // A BigInteger comprised of 32 Bytes; toByteArray has 32 bytes; 1 extra padding added + Arguments.of("49026876502144691037964633390198098042987098960613207831256521276903508291997", // BigInt + "6C643A8EB51D365F3FF5B08C575DEA44B0D3CA5795BDD7B080A7057ABB9A319D", // .toString(16) + "6C643A8EB51D365F3FF5B08C575DEA44B0D3CA5795BDD7B080A7057ABB9A319D", // .toByteArray() + "006C643A8EB51D365F3FF5B08C575DEA44B0D3CA5795BDD7B080A7057ABB9A319D"// <-- BigInteger Hex, Padded to 33 bytes + ), + // A BigInteger comprised of 31 Bytes; toByteArray has 31 bytes; 2 extra padding added + Arguments.of("125364023161033659590032058970590371956067907570302268576097734468145372487", // BigInt + "46F41A0ECE7D0C61B5B36EA377E20621E23C13BD0ABBAEF80754180E9DDD47", // .toString(16) + "46F41A0ECE7D0C61B5B36EA377E20621E23C13BD0ABBAEF80754180E9DDD47", // .toByteArray() + "000046F41A0ECE7D0C61B5B36EA377E20621E23C13BD0ABBAEF80754180E9DDD47"// <-- BigInteger Hex, Padded to 33 bytes + ), + // A BigInteger comprised of 31 Bytes; toByteArray has 31 bytes; 2 extra padding added + Arguments.of("194815934319126504488398097255143744553440248783815166056530734282223472643", // BigInt + "6E430C9E47DFB2194C97385CC85C406DC69773145AE5DE6060821AAAAD5003", // .toString(16) + "6E430C9E47DFB2194C97385CC85C406DC69773145AE5DE6060821AAAAD5003", // .toByteArray() + "00006E430C9E47DFB2194C97385CC85C406DC69773145AE5DE6060821AAAAD5003"// <-- BigInteger Hex, Padded to 33 bytes + ), + // A BigInteger comprised of 30 Bytes; toByteArray has 30 bytes; 3 extra padding added + Arguments.of("116983811426126878045574354873599490265363256342001470285420476536129339", // BigInt + "10F32BB4E0B8B00469196EDACCCAA87A55409FF1C66330D7590449C7073B", // .toString(16) + "10F32BB4E0B8B00469196EDACCCAA87A55409FF1C66330D7590449C7073B", // .toByteArray() + "00000010F32BB4E0B8B00469196EDACCCAA87A55409FF1C66330D7590449C7073B" // <-- BigInteger Hex, Padded to 33 bytes + ), + // A BigInteger comprised of 30 Bytes; toByteArray has 30 bytes; 3 extra padding added + Arguments.of("95191719494323154714287471792160232496133380576373117355131561137428728", // BigInt + "DCADB6BD9E78F0ECE39BB26928F8E4B2A5C7F9CF62C15C1C554B5F458F8", // .toString(16) + "0DCADB6BD9E78F0ECE39BB26928F8E4B2A5C7F9CF62C15C1C554B5F458F8", // .toByteArray() + "0000000DCADB6BD9E78F0ECE39BB26928F8E4B2A5C7F9CF62C15C1C554B5F458F8" // <-- BigInteger Hex, Padded to 33 bytes + ) + ); + } + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoRequestParamsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoRequestParamsTest.java new file mode 100644 index 000000000..a06cbaece --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoRequestParamsTest.java @@ -0,0 +1,46 @@ +package org.xrpl.xrpl4j.model.client.amm; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.ledger.Issue; +import org.xrpl.xrpl4j.model.transactions.Address; + +class AmmInfoRequestParamsTest extends AbstractJsonTest { + + @Test + void testAssetAsset2Json() throws JSONException, JsonProcessingException { + AmmInfoRequestParams params = AmmInfoRequestParams.from( + Issue.XRP, + Issue.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .build() + ); + String json = "{\n" + + " \"asset\": {\n" + + " \"currency\": \"XRP\"\n" + + " },\n" + + " \"asset2\": {\n" + + " \"currency\": \"TST\",\n" + + " \"issuer\": \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " }\n" + + " }"; + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testAmmAccountJson() throws JSONException, JsonProcessingException { + AmmInfoRequestParams params = AmmInfoRequestParams.from( + Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd") + ); + + String json = "{\n" + + " \"amm_account\": \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " }"; + + assertCanSerializeAndDeserialize(params, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoResultTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoResultTest.java new file mode 100644 index 000000000..70818b568 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/amm/AmmInfoResultTest.java @@ -0,0 +1,231 @@ +package org.xrpl.xrpl4j.model.client.amm; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.client.common.LedgerIndex; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Hash256; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TradingFee; +import org.xrpl.xrpl4j.model.transactions.VoteWeight; +import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +class AmmInfoResultTest extends AbstractJsonTest { + + @Test + void testJsonForCurrentLedger() throws JSONException, JsonProcessingException { + AmmInfoResult result = AmmInfoResult.builder() + .amm( + AmmInfo.builder() + .account(Address.of("rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze")) + .amount(XrpCurrencyAmount.ofDrops(11080000720L)) + .amount2( + IssuedCurrencyAmount.builder() + .currency("USD") + .issuer(Address.of("rELH2VCCkjDzvygtB4nKiqGav7h53RhDiP")) + .value("11080.00072727936") + .build() + ) + .auctionSlot( + AmmInfoAuctionSlot.builder() + .account(Address.of("rM7xXGzMUALmEhQ2y9FW5XG69WXwQ6xtDC")) + .addAuthAccounts( + AmmInfoAuthAccount.of(Address.of("rHq1eC9TEyEPVhRvdTPLKr3z8D5BUzcHqi")), + AmmInfoAuthAccount.of(Address.of("rNzgpEGUyEmQ1YGDMAiGGBvwtzbk78tcCG")) + ) + .discountedFee(TradingFee.of(UnsignedInteger.ZERO)) + .expiration( + ZonedDateTime.parse( + "2023-07-20T15:17:31+0000", + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US) + ).withZoneSameLocal(ZoneId.of("UTC")) + ) + .price( + IssuedCurrencyAmount.builder() + .currency("03930D02208264E2E40EC1B0C09E4DB96EE197B1") + .issuer(Address.of("rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze")) + .value("100") + .build() + ) + .timeInterval(UnsignedInteger.ZERO) + .build() + ) + .lpToken( + IssuedCurrencyAmount.builder() + .currency("03930D02208264E2E40EC1B0C09E4DB96EE197B1") + .issuer(Address.of("rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze")) + .value("11079900") + .build() + ) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(225))) + .addVoteSlots( + AmmInfoVoteEntry.builder() + .voteWeight(VoteWeight.of(UnsignedInteger.valueOf(90))) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(50))) + .account(Address.of("rs6HZNabrZzBBjDWCwkWcSGdDH7Xsi4Z99")) + .build(), + AmmInfoVoteEntry.builder() + .voteWeight(VoteWeight.of(UnsignedInteger.valueOf(90))) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(100))) + .account(Address.of("rJd7rhLSaqLHEfeqAW2vYzYYkhvyE9XfBE")) + .build() + ) + .build() + ) + .ledgerCurrentIndex(LedgerIndex.of(UnsignedInteger.valueOf(102))) + .status("success") + .build(); + + String json = "{\"amm\": {\"account\": \"rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze\",\n" + + " \"amount\": \"11080000720\",\n" + + " \"amount2\": {\"currency\": \"USD\",\n" + + " \"issuer\": \"rELH2VCCkjDzvygtB4nKiqGav7h53RhDiP\",\n" + + " \"value\": \"11080.00072727936\"},\n" + + " \"asset2_frozen\": false,\n" + + " \"asset_frozen\": false,\n" + + " \"auction_slot\": {\"account\": \"rM7xXGzMUALmEhQ2y9FW5XG69WXwQ6xtDC\",\n" + + " \"auth_accounts\": [{\"account\": \"rHq1eC9TEyEPVhRvdTPLKr3z8D5BUzcHqi\"},\n" + + " {\"account\": \"rNzgpEGUyEmQ1YGDMAiGGBvwtzbk78tcCG\"}],\n" + + " \"discounted_fee\": 0,\n" + + " \"expiration\": \"2023-07-20T15:17:31+0000\",\n" + + " \"price\": {\"currency\": \"03930D02208264E2E40EC1B0C09E4DB96EE197B1\",\n" + + " \"issuer\": \"rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze\",\n" + + " \"value\": \"100\"},\n" + + " \"time_interval\": 0},\n" + + " \"lp_token\": {\"currency\": \"03930D02208264E2E40EC1B0C09E4DB96EE197B1\",\n" + + " \"issuer\": \"rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze\",\n" + + " \"value\": \"11079900\"},\n" + + " \"trading_fee\": 225,\n" + + " \"vote_slots\": [{\"account\": \"rs6HZNabrZzBBjDWCwkWcSGdDH7Xsi4Z99\",\n" + + " \"trading_fee\": 50,\n" + + " \"vote_weight\": 90},\n" + + " {\"account\": \"rJd7rhLSaqLHEfeqAW2vYzYYkhvyE9XfBE\",\n" + + " \"trading_fee\": 100,\n" + + " \"vote_weight\": 90}]},\n" + + " \"ledger_current_index\": 102,\n" + + " \"status\": \"success\",\n" + + " \"validated\": false}"; + + assertCanSerializeAndDeserialize(result, json); + + assertThat(result.ledgerCurrentIndexSafe()).isEqualTo(result.ledgerCurrentIndex().get()); + assertThatThrownBy(result::ledgerIndexSafe).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(result::ledgerHashSafe).isInstanceOf(IllegalStateException.class); + } + + @Test + void testJsonForValidatedLedger() throws JSONException, JsonProcessingException { + AmmInfoResult result = AmmInfoResult.builder() + .amm( + AmmInfo.builder() + .account(Address.of("rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze")) + .amount(XrpCurrencyAmount.ofDrops(11080000720L)) + .amount2( + IssuedCurrencyAmount.builder() + .currency("USD") + .issuer(Address.of("rELH2VCCkjDzvygtB4nKiqGav7h53RhDiP")) + .value("11080.00072727936") + .build() + ) + .asset2Frozen(false) + .auctionSlot( + AmmInfoAuctionSlot.builder() + .account(Address.of("rM7xXGzMUALmEhQ2y9FW5XG69WXwQ6xtDC")) + .addAuthAccounts( + AmmInfoAuthAccount.of(Address.of("rHq1eC9TEyEPVhRvdTPLKr3z8D5BUzcHqi")), + AmmInfoAuthAccount.of(Address.of("rNzgpEGUyEmQ1YGDMAiGGBvwtzbk78tcCG")) + ) + .discountedFee(TradingFee.of(UnsignedInteger.ZERO)) + .expiration( + ZonedDateTime.parse( + "2023-07-20T15:17:31+0000", + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US) + ).withZoneSameLocal(ZoneId.of("UTC")) + ) + .price( + IssuedCurrencyAmount.builder() + .currency("03930D02208264E2E40EC1B0C09E4DB96EE197B1") + .issuer(Address.of("rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze")) + .value("100") + .build() + ) + .timeInterval(UnsignedInteger.ZERO) + .build() + ) + .lpToken( + IssuedCurrencyAmount.builder() + .currency("03930D02208264E2E40EC1B0C09E4DB96EE197B1") + .issuer(Address.of("rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze")) + .value("11079900") + .build() + ) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(225))) + .addVoteSlots( + AmmInfoVoteEntry.builder() + .voteWeight(VoteWeight.of(UnsignedInteger.valueOf(90))) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(50))) + .account(Address.of("rs6HZNabrZzBBjDWCwkWcSGdDH7Xsi4Z99")) + .build(), + AmmInfoVoteEntry.builder() + .voteWeight(VoteWeight.of(UnsignedInteger.valueOf(90))) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(100))) + .account(Address.of("rJd7rhLSaqLHEfeqAW2vYzYYkhvyE9XfBE")) + .build() + ) + .build() + ) + .ledgerHash(Hash256.of("93586177048F82080AB79B8D0FA76F9D93AF458551A7358D9F0EC6D790AF5CBA")) + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(102))) + .status("success") + .validated(true) + .build(); + + String json = "{\"amm\": {\"account\": \"rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze\",\n" + + " \"amount\": \"11080000720\",\n" + + " \"amount2\": {\"currency\": \"USD\",\n" + + " \"issuer\": \"rELH2VCCkjDzvygtB4nKiqGav7h53RhDiP\",\n" + + " \"value\": \"11080.00072727936\"},\n" + + " \"asset2_frozen\": false,\n" + + " \"asset_frozen\": false,\n" + + " \"auction_slot\": {\"account\": \"rM7xXGzMUALmEhQ2y9FW5XG69WXwQ6xtDC\",\n" + + " \"auth_accounts\": [{\"account\": \"rHq1eC9TEyEPVhRvdTPLKr3z8D5BUzcHqi\"},\n" + + " {\"account\": \"rNzgpEGUyEmQ1YGDMAiGGBvwtzbk78tcCG\"}],\n" + + " \"discounted_fee\": 0,\n" + + " \"expiration\": \"2023-07-20T15:17:31+0000\",\n" + + " \"price\": {\"currency\": \"03930D02208264E2E40EC1B0C09E4DB96EE197B1\",\n" + + " \"issuer\": \"rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze\",\n" + + " \"value\": \"100\"},\n" + + " \"time_interval\": 0},\n" + + " \"lp_token\": {\"currency\": \"03930D02208264E2E40EC1B0C09E4DB96EE197B1\",\n" + + " \"issuer\": \"rU3auoTuhaPwiiod3wEXNnYogxMnYsBhze\",\n" + + " \"value\": \"11079900\"},\n" + + " \"trading_fee\": 225,\n" + + " \"vote_slots\": [{\"account\": \"rs6HZNabrZzBBjDWCwkWcSGdDH7Xsi4Z99\",\n" + + " \"trading_fee\": 50,\n" + + " \"vote_weight\": 90},\n" + + " {\"account\": \"rJd7rhLSaqLHEfeqAW2vYzYYkhvyE9XfBE\",\n" + + " \"trading_fee\": 100,\n" + + " \"vote_weight\": 90}]},\n" + + " \"ledger_hash\": \"93586177048F82080AB79B8D0FA76F9D93AF458551A7358D9F0EC6D790AF5CBA\",\n" + + " \"ledger_index\": 102,\n" + + " \"status\": \"success\",\n" + + " \"validated\": true}"; + + assertCanSerializeAndDeserialize(result, json); + + assertThat(result.ledgerIndexSafe()).isEqualTo(result.ledgerIndex().get()); + assertThat(result.ledgerHashSafe()).isEqualTo(result.ledgerHash().get()); + assertThatThrownBy(result::ledgerCurrentIndexSafe).isInstanceOf(IllegalStateException.class); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParamsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParamsTest.java new file mode 100644 index 000000000..1955540d3 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParamsTest.java @@ -0,0 +1,432 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.xrpl.xrpl4j.crypto.TestConstants.ED_ADDRESS; +import static org.xrpl.xrpl4j.crypto.TestConstants.HASH_256; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.client.XrplRequestParams; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; +import org.xrpl.xrpl4j.model.client.ledger.RippleStateLedgerEntryParams.RippleStateAccounts; +import org.xrpl.xrpl4j.model.ledger.AccountRootObject; +import org.xrpl.xrpl4j.model.ledger.AmmObject; +import org.xrpl.xrpl4j.model.ledger.CheckObject; +import org.xrpl.xrpl4j.model.ledger.DepositPreAuthObject; +import org.xrpl.xrpl4j.model.ledger.EscrowObject; +import org.xrpl.xrpl4j.model.ledger.Issue; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; +import org.xrpl.xrpl4j.model.ledger.NfTokenPageObject; +import org.xrpl.xrpl4j.model.ledger.OfferObject; +import org.xrpl.xrpl4j.model.ledger.PayChannelObject; +import org.xrpl.xrpl4j.model.ledger.RippleStateObject; +import org.xrpl.xrpl4j.model.ledger.TicketObject; +import org.xrpl.xrpl4j.model.transactions.Address; + +class LedgerEntryRequestParamsTest extends AbstractJsonTest { + + @Test + void testTypedIndexParams() throws JSONException, JsonProcessingException { + LedgerEntryRequestParams params = LedgerEntryRequestParams.index(HASH_256, AmmObject.class, + LedgerSpecifier.VALIDATED); + assertThat(params.index()).isNotEmpty().get().isEqualTo(HASH_256); + assertThat(params.ledgerObjectClass()).isEqualTo(AmmObject.class); + + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = String.format("{\n" + + " \"index\": \"%s\",\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }", HASH_256); + + String serialized = objectMapper.writeValueAsString(params); + JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); + + // Note that when deserializing from JSON, we cannot figure out what ledgerObjectClass should be based on the JSON. + // This is likely fine because request params should never really be getting deserialized by this library. + XrplRequestParams deserialized = objectMapper.readValue(serialized, params.getClass()); + assertThat(deserialized).usingRecursiveComparison().ignoringFields("ledgerObjectClass") + .isEqualTo(params); + } + + @Test + void testUntypedIndexParams() throws JSONException, JsonProcessingException { + LedgerEntryRequestParams params = LedgerEntryRequestParams.index(HASH_256, LedgerSpecifier.VALIDATED); + assertThat(params.index()).isNotEmpty().get().isEqualTo(HASH_256); + assertThat(params.ledgerObjectClass()).isEqualTo(LedgerObject.class); + + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = String.format("{\n" + + " \"index\": \"%s\",\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }", HASH_256); + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testAccountRootParams() throws JSONException, JsonProcessingException { + LedgerEntryRequestParams params = LedgerEntryRequestParams.accountRoot( + ED_ADDRESS, LedgerSpecifier.VALIDATED + ); + assertThat(params.accountRoot()).isNotEmpty().get().isEqualTo(ED_ADDRESS); + assertThat(params.ledgerObjectClass()).isEqualTo(AccountRootObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = String.format("{\n" + + " \"account_root\": \"%s\",\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }", ED_ADDRESS); + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testAmmParams() throws JSONException, JsonProcessingException { + AmmLedgerEntryParams ammParams = AmmLedgerEntryParams.builder() + .asset(Issue.XRP) + .asset2( + Issue.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .build() + ) + .build(); + + LedgerEntryRequestParams params = LedgerEntryRequestParams.amm(ammParams, LedgerSpecifier.VALIDATED); + assertThat(params.amm()).isNotEmpty().get().isEqualTo(ammParams); + assertThat(params.ledgerObjectClass()).isEqualTo(AmmObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = "{\n" + + " \"amm\": {\n" + + " \"asset\": {\n" + + " \"currency\": \"XRP\"\n" + + " },\n" + + " \"asset2\": {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " }\n" + + " },\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }"; + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testOfferParams() throws JSONException, JsonProcessingException { + OfferLedgerEntryParams offerParams = OfferLedgerEntryParams.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .seq(UnsignedInteger.valueOf(359)) + .build(); + + LedgerEntryRequestParams params = LedgerEntryRequestParams.offer( + offerParams, LedgerSpecifier.VALIDATED + ); + assertThat(params.offer()).isNotEmpty().get().isEqualTo(offerParams); + assertThat(params.ledgerObjectClass()).isEqualTo(OfferObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = "{\n" + + " \"offer\": {\n" + + " \"account\": \"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\",\n" + + " \"seq\": 359\n" + + " },\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }"; + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testRippleStateParams() throws JSONException, JsonProcessingException { + RippleStateLedgerEntryParams rippleStateParams = RippleStateLedgerEntryParams.builder() + .accounts(RippleStateAccounts.of( + Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"), + Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW") + )) + .currency("USD") + .build(); + + LedgerEntryRequestParams params = LedgerEntryRequestParams.rippleState( + rippleStateParams, LedgerSpecifier.VALIDATED + ); + assertThat(params.rippleState()).isNotEmpty().get().isEqualTo(rippleStateParams); + assertThat(params.ledgerObjectClass()).isEqualTo(RippleStateObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = "{\n" + + " \"ripple_state\": {\n" + + " \"accounts\": [\n" + + " \"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\",\n" + + " \"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW\"\n" + + " ],\n" + + " \"currency\": \"USD\"\n" + + " },\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }"; + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testCheckParams() throws JSONException, JsonProcessingException { + LedgerEntryRequestParams params = LedgerEntryRequestParams.check(HASH_256, LedgerSpecifier.VALIDATED); + assertThat(params.check()).isNotEmpty().get().isEqualTo(HASH_256); + assertThat(params.ledgerObjectClass()).isEqualTo(CheckObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = String.format("{\n" + + " \"check\": \"%s\",\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }", HASH_256); + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testEscrowParams() throws JSONException, JsonProcessingException { + EscrowLedgerEntryParams escrowParams = EscrowLedgerEntryParams.builder() + .owner(Address.of("rL4fPHi2FWGwRGRQSH7gBcxkuo2b9NTjKK")) + .seq(UnsignedInteger.valueOf(126)) + .build(); + LedgerEntryRequestParams params = LedgerEntryRequestParams.escrow( + escrowParams, LedgerSpecifier.VALIDATED + ); + assertThat(params.escrow()).isNotEmpty().get().isEqualTo(escrowParams); + assertThat(params.ledgerObjectClass()).isEqualTo(EscrowObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = "{\n" + + " \"escrow\": {\n" + + " \"owner\": \"rL4fPHi2FWGwRGRQSH7gBcxkuo2b9NTjKK\",\n" + + " \"seq\": 126\n" + + " },\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }"; + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testPaymentChannelParams() throws JSONException, JsonProcessingException { + LedgerEntryRequestParams params = LedgerEntryRequestParams.paymentChannel( + HASH_256, LedgerSpecifier.VALIDATED + ); + assertThat(params.paymentChannel()).isNotEmpty().get().isEqualTo(HASH_256); + assertThat(params.ledgerObjectClass()).isEqualTo(PayChannelObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = String.format("{\n" + + " \"payment_channel\": \"%s\",\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }", HASH_256); + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testDepositPreAuthParams() throws JSONException, JsonProcessingException { + DepositPreAuthLedgerEntryParams depositPreAuthParams = DepositPreAuthLedgerEntryParams.builder() + .owner(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .authorized(Address.of("ra5nK24KXen9AHvsdFTKHSANinZseWnPcX")) + .build(); + LedgerEntryRequestParams params = LedgerEntryRequestParams.depositPreAuth( + depositPreAuthParams, + LedgerSpecifier.VALIDATED + ); + assertThat(params.depositPreAuth()).isNotEmpty().get().isEqualTo(depositPreAuthParams); + assertThat(params.ledgerObjectClass()).isEqualTo(DepositPreAuthObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = "{\n" + + " \"deposit_preauth\": {\n" + + " \"owner\": \"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\",\n" + + " \"authorized\": \"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX\"\n" + + " },\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }"; + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testTicketParams() throws JSONException, JsonProcessingException { + TicketLedgerEntryParams ticketParams = TicketLedgerEntryParams.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .ticketSeq(UnsignedInteger.valueOf(389)) + .build(); + LedgerEntryRequestParams params = LedgerEntryRequestParams.ticket( + ticketParams, + LedgerSpecifier.VALIDATED + ); + assertThat(params.ticket()).isNotEmpty().get().isEqualTo(ticketParams); + assertThat(params.ledgerObjectClass()).isEqualTo(TicketObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + + String json = "{\n" + + " \"ticket\": {\n" + + " \"account\": \"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\",\n" + + " \"ticket_seq\": 389\n" + + " },\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }"; + + assertCanSerializeAndDeserialize(params, json); + } + + @Test + void testNftPageParams() throws JSONException, JsonProcessingException { + LedgerEntryRequestParams params = LedgerEntryRequestParams.nftPage( + HASH_256, + LedgerSpecifier.VALIDATED + ); + assertThat(params.nftPage()).isNotEmpty().get().isEqualTo(HASH_256); + assertThat(params.ledgerObjectClass()).isEqualTo(NfTokenPageObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + + String json = String.format("{\n" + + " \"nft_page\": \"%s\",\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }", HASH_256); + + assertCanSerializeAndDeserialize(params, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResultTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResultTest.java new file mode 100644 index 000000000..8bcfb0a8d --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResultTest.java @@ -0,0 +1,676 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.xrpl.xrpl4j.crypto.TestConstants.HASH_256; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import com.google.common.primitives.UnsignedLong; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.client.common.LedgerIndex; +import org.xrpl.xrpl4j.model.flags.AccountRootFlags; +import org.xrpl.xrpl4j.model.flags.OfferFlags; +import org.xrpl.xrpl4j.model.flags.RippleStateFlags; +import org.xrpl.xrpl4j.model.ledger.AccountRootObject; +import org.xrpl.xrpl4j.model.ledger.AmmObject; +import org.xrpl.xrpl4j.model.ledger.AuctionSlot; +import org.xrpl.xrpl4j.model.ledger.CheckObject; +import org.xrpl.xrpl4j.model.ledger.DepositPreAuthObject; +import org.xrpl.xrpl4j.model.ledger.EscrowObject; +import org.xrpl.xrpl4j.model.ledger.Issue; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; +import org.xrpl.xrpl4j.model.ledger.NfToken; +import org.xrpl.xrpl4j.model.ledger.NfTokenPageObject; +import org.xrpl.xrpl4j.model.ledger.NfTokenWrapper; +import org.xrpl.xrpl4j.model.ledger.OfferObject; +import org.xrpl.xrpl4j.model.ledger.PayChannelObject; +import org.xrpl.xrpl4j.model.ledger.RippleStateObject; +import org.xrpl.xrpl4j.model.ledger.TicketObject; +import org.xrpl.xrpl4j.model.ledger.VoteEntry; +import org.xrpl.xrpl4j.model.ledger.VoteEntryWrapper; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Hash256; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.NfTokenId; +import org.xrpl.xrpl4j.model.transactions.NfTokenUri; +import org.xrpl.xrpl4j.model.transactions.TradingFee; +import org.xrpl.xrpl4j.model.transactions.VoteWeight; +import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; + +class LedgerEntryResultTest extends AbstractJsonTest { + + @Test + void testAccountRootResult() throws JSONException, JsonProcessingException { + LedgerEntryResult result = LedgerEntryResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(83125250))) + .ledgerHash(Hash256.of("783625588CF01BD3D0E9C2719B92098A6A87649AEFF5AE970CD68B911436C1D7")) + .validated(true) + .index(Hash256.of("13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8")) + .node( + AccountRootObject.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .accountTransactionId(Hash256.of("932CC7E9BAC1F7B9FA5381679F293EEC0A646E5E7F2F6D14C85FEE2102F0E66C")) + .balance(XrpCurrencyAmount.ofDrops(1066107694)) + .domain("6D64756F31332E636F6D") + .emailHash("98B4375E1D753E5B91627516F6D70977") + .flags(AccountRootFlags.of(9568256)) + .messageKey("0000000000000000000000070000000300") + .ownerCount(UnsignedInteger.valueOf(17)) + .previousTransactionId(Hash256.of("7E5F3FB60E1177F8AF8A9EAC7982F27FA5494FDEA871B23B4B149939A5A7A7BB")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(82357607)) + .regularKey(Address.of("rD9iJmieYHn8jTtPjwwkW2Wm9sVDvPXLoJ")) + .sequence(UnsignedInteger.valueOf(393)) + .ticketCount(UnsignedInteger.valueOf(5)) + .transferRate(UnsignedInteger.valueOf(4294967295L)) + .index(Hash256.of("13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8")) + .build() + ) + .status("success") + .build(); + + String json = "{\n" + + " \"ledger_hash\": \"783625588CF01BD3D0E9C2719B92098A6A87649AEFF5AE970CD68B911436C1D7\",\n" + + " \"ledger_index\": 83125250,\n" + + " \"validated\": true,\n" + + " \"index\": \"13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8\",\n" + + " \"node\": {\n" + + " \"Account\": \"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\",\n" + + " \"AccountTxnID\": \"932CC7E9BAC1F7B9FA5381679F293EEC0A646E5E7F2F6D14C85FEE2102F0E66C\",\n" + + " \"Balance\": \"1066107694\",\n" + + " \"Domain\": \"6D64756F31332E636F6D\",\n" + + " \"EmailHash\": \"98B4375E1D753E5B91627516F6D70977\",\n" + + " \"Flags\": 9568256,\n" + + " \"LedgerEntryType\": \"AccountRoot\",\n" + + " \"MessageKey\": \"0000000000000000000000070000000300\",\n" + + " \"OwnerCount\": 17,\n" + + " \"PreviousTxnID\": \"7E5F3FB60E1177F8AF8A9EAC7982F27FA5494FDEA871B23B4B149939A5A7A7BB\",\n" + + " \"PreviousTxnLgrSeq\": 82357607,\n" + + " \"RegularKey\": \"rD9iJmieYHn8jTtPjwwkW2Wm9sVDvPXLoJ\",\n" + + " \"Sequence\": 393,\n" + + " \"TicketCount\": 5,\n" + + " \"TransferRate\": 4294967295,\n" + + " \"index\": \"13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8\"\n" + + " },\n" + + " \"status\": \"success\"\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testAmmResult() throws JSONException, JsonProcessingException { + LedgerEntryResult result = LedgerEntryResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(607272))) + .ledgerHash(Hash256.of("EEB650A0FD3CF0A5CE68B3DBD67C902FEC85E6AFAE1D0A7A7AF4BAD2F38557C7")) + .validated(true) + .index(Hash256.of("6BCD7E451DDA015FB307DAD9208A98A2DC3AC4D1448E624B42C89246DCF08692")) + .node( + AmmObject.builder() + .account(Address.of("rNqXnvSYbjZeJQ6jWcf6T5mnNMRPzHXaZW")) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .currency("7872706C346A436F696E00000000000000000000") + .issuer(Address.of("rDeo7rDoYw6AUKGneWwfkHPsMJagxcGWy1")) + .build() + ) + .auctionSlot( + AuctionSlot.builder() + .account(Address.of("rDeo7rDoYw6AUKGneWwfkHPsMJagxcGWy1")) + .discountedFee(TradingFee.of(UnsignedInteger.valueOf(77))) + .expiration(UnsignedInteger.valueOf(750359162)) + .price( + IssuedCurrencyAmount.builder() + .currency("03DCF8F3910BFE6AB56136A90BD41E0902E23C4F") + .value("0") + .issuer(Address.of("rNqXnvSYbjZeJQ6jWcf6T5mnNMRPzHXaZW")) + .build() + ) + .build() + ) + .lpTokenBalance( + IssuedCurrencyAmount.builder() + .currency("03DCF8F3910BFE6AB56136A90BD41E0902E23C4F") + .issuer(Address.of("rNqXnvSYbjZeJQ6jWcf6T5mnNMRPzHXaZW")) + .value("70606.68056410846") + .build() + ) + .ownerNode("0") + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(778))) + .addVoteSlots( + VoteEntryWrapper.of(VoteEntry.builder() + .account(Address.of("rDeo7rDoYw6AUKGneWwfkHPsMJagxcGWy1")) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(1000))) + .voteWeight(VoteWeight.of(UnsignedInteger.valueOf(70815))) + .build()), + VoteEntryWrapper.of(VoteEntry.builder() + .account(Address.of("rHPoJo9R3QdQjK6XdWL5hY2eTc4wUeNYzW")) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(240))) + .voteWeight(VoteWeight.of(UnsignedInteger.valueOf(29185))) + .build()) + ) + .index(Hash256.of("6BCD7E451DDA015FB307DAD9208A98A2DC3AC4D1448E624B42C89246DCF08692")) + .build() + ) + .status("success") + .build(); + + String json = "{\n" + + " \"index\": \"6BCD7E451DDA015FB307DAD9208A98A2DC3AC4D1448E624B42C89246DCF08692\",\n" + + " \"ledger_hash\": \"EEB650A0FD3CF0A5CE68B3DBD67C902FEC85E6AFAE1D0A7A7AF4BAD2F38557C7\",\n" + + " \"ledger_index\": 607272,\n" + + " \"node\": {\n" + + " \"Account\": \"rNqXnvSYbjZeJQ6jWcf6T5mnNMRPzHXaZW\",\n" + + " \"Asset\": {\n" + + " \"currency\": \"XRP\"\n" + + " },\n" + + " \"Asset2\": {\n" + + " \"currency\": \"7872706C346A436F696E00000000000000000000\",\n" + + " \"issuer\": \"rDeo7rDoYw6AUKGneWwfkHPsMJagxcGWy1\"\n" + + " },\n" + + " \"AuctionSlot\": {\n" + + " \"Account\": \"rDeo7rDoYw6AUKGneWwfkHPsMJagxcGWy1\",\n" + + " \"DiscountedFee\": 77,\n" + + " \"Expiration\": 750359162,\n" + + " \"Price\": {\n" + + " \"currency\": \"03DCF8F3910BFE6AB56136A90BD41E0902E23C4F\",\n" + + " \"issuer\": \"rNqXnvSYbjZeJQ6jWcf6T5mnNMRPzHXaZW\",\n" + + " \"value\": \"0\"\n" + + " }\n" + + " },\n" + + " \"Flags\": 0,\n" + + " \"LPTokenBalance\": {\n" + + " \"currency\": \"03DCF8F3910BFE6AB56136A90BD41E0902E23C4F\",\n" + + " \"issuer\": \"rNqXnvSYbjZeJQ6jWcf6T5mnNMRPzHXaZW\",\n" + + " \"value\": \"70606.68056410846\"\n" + + " },\n" + + " \"LedgerEntryType\": \"AMM\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"TradingFee\": 778,\n" + + " \"VoteSlots\": [\n" + + " {\n" + + " \"VoteEntry\": {\n" + + " \"Account\": \"rDeo7rDoYw6AUKGneWwfkHPsMJagxcGWy1\",\n" + + " \"TradingFee\": 1000,\n" + + " \"VoteWeight\": 70815\n" + + " }\n" + + " },\n" + + " {\n" + + " \"VoteEntry\": {\n" + + " \"Account\": \"rHPoJo9R3QdQjK6XdWL5hY2eTc4wUeNYzW\",\n" + + " \"TradingFee\": 240,\n" + + " \"VoteWeight\": 29185\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"index\": \"6BCD7E451DDA015FB307DAD9208A98A2DC3AC4D1448E624B42C89246DCF08692\"\n" + + " },\n" + + " \"status\": \"success\",\n" + + " \"validated\": true\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testOfferResult() throws JSONException, JsonProcessingException { + LedgerEntryResult result = LedgerEntryResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(41931093))) + .ledgerHash(Hash256.of("54FE89D2FF925D623D386A03B402FDB22B2D2D058A62AEE441CA52CC9AA92BB1")) + .validated(true) + .index(Hash256.of("066B61CF7248A5A08672541077E3C58EAFD1FA52DDF6B4FD93595E16542C0A14")) + .node( + OfferObject.builder() + .account(Address.of("rNdCZMZqHCo5VkrvsmNVt8ZtdpahT7rDKx")) + .bookDirectory(Hash256.of("D30EF7A9BFCCEE47AF722871D91E1E21522DF5141CA29AB05B071AFD498D0000")) + .bookNode("0") + .flags(OfferFlags.of(131072)) + .ownerNode("0") + .previousTransactionId(Hash256.of("9D613D7E1E34DA5BE421E06002441F1E183C0B7B3323E1898FC585144A7C13B1")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(41931065)) + .sequence(UnsignedInteger.valueOf(41931063)) + .takerGets( + IssuedCurrencyAmount.builder() + .currency("USD") + .issuer(Address.of("rNdCZMZqHCo5VkrvsmNVt8ZtdpahT7rDKx")) + .value("100") + .build() + ) + .takerPays(XrpCurrencyAmount.ofDrops(200000000)) + .index(Hash256.of("066B61CF7248A5A08672541077E3C58EAFD1FA52DDF6B4FD93595E16542C0A14")) + .build() + ) + .status("success") + .build(); + + String json = "{\n" + + " \"index\": \"066B61CF7248A5A08672541077E3C58EAFD1FA52DDF6B4FD93595E16542C0A14\",\n" + + " \"ledger_hash\": \"54FE89D2FF925D623D386A03B402FDB22B2D2D058A62AEE441CA52CC9AA92BB1\",\n" + + " \"ledger_index\": 41931093,\n" + + " \"node\": {\n" + + " \"Account\": \"rNdCZMZqHCo5VkrvsmNVt8ZtdpahT7rDKx\",\n" + + " \"BookDirectory\": \"D30EF7A9BFCCEE47AF722871D91E1E21522DF5141CA29AB05B071AFD498D0000\",\n" + + " \"BookNode\": \"0\",\n" + + " \"Flags\": 131072,\n" + + " \"LedgerEntryType\": \"Offer\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"PreviousTxnID\": \"9D613D7E1E34DA5BE421E06002441F1E183C0B7B3323E1898FC585144A7C13B1\",\n" + + " \"PreviousTxnLgrSeq\": 41931065,\n" + + " \"Sequence\": 41931063,\n" + + " \"TakerGets\": {\n" + + " \"currency\": \"USD\",\n" + + " \"issuer\": \"rNdCZMZqHCo5VkrvsmNVt8ZtdpahT7rDKx\",\n" + + " \"value\": \"100\"\n" + + " },\n" + + " \"TakerPays\": \"200000000\",\n" + + " \"index\": \"066B61CF7248A5A08672541077E3C58EAFD1FA52DDF6B4FD93595E16542C0A14\"\n" + + " },\n" + + " \"status\": \"success\",\n" + + " \"validated\": true\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testRippleStateResult() throws JSONException, JsonProcessingException { + LedgerEntryResult result = LedgerEntryResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(83126482))) + .ledgerHash(Hash256.of("995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84")) + .validated(true) + .index(Hash256.of("6A409D31A016227B74D6A14C307239B2BBBE0CFBFCF7C271BFAF20CAA7A1E6DA")) + .node( + RippleStateObject.builder() + .balance( + IssuedCurrencyAmount.builder() + .currency("CNY") + .issuer(Address.of("rrrrrrrrrrrrrrrrrrrrBZbvji")) + .value("0") + .build() + ) + .flags(RippleStateFlags.of(2228224)) + .highLimit( + IssuedCurrencyAmount.builder() + .currency("CNY") + .issuer(Address.of("rHzKtpcB1KC1YuU4PBhk9m2abqrf2kZsfV")) + .value("1000000000") + .build() + ) + .highNode("0") + .lowLimit( + IssuedCurrencyAmount.builder() + .currency("CNY") + .issuer(Address.of("rJ1adrpGS3xsnQMb9Cw54tWJVFPuSdZHK")) + .value("0") + .build() + ) + .lowNode("2") + .previousTransactionId(Hash256.of("9A5E68C795D68665A648A5A05E5BC94AA3681400353236F75139BD102D9406FD")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(69746363)) + .index(Hash256.of("6A409D31A016227B74D6A14C307239B2BBBE0CFBFCF7C271BFAF20CAA7A1E6DA")) + .build() + ) + .status("success") + .build(); + + String json = "{\n" + + " \"ledger_hash\": \"995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84\",\n" + + " \"ledger_index\": 83126482,\n" + + " \"validated\": true,\n" + + " \"index\": \"6A409D31A016227B74D6A14C307239B2BBBE0CFBFCF7C271BFAF20CAA7A1E6DA\",\n" + + " \"node\": {\n" + + " \"Balance\": {\n" + + " \"currency\": \"CNY\",\n" + + " \"issuer\": \"rrrrrrrrrrrrrrrrrrrrBZbvji\",\n" + + " \"value\": \"0\"\n" + + " },\n" + + " \"Flags\": 2228224,\n" + + " \"HighLimit\": {\n" + + " \"currency\": \"CNY\",\n" + + " \"issuer\": \"rHzKtpcB1KC1YuU4PBhk9m2abqrf2kZsfV\",\n" + + " \"value\": \"1000000000\"\n" + + " },\n" + + " \"HighNode\": \"0\",\n" + + " \"LedgerEntryType\": \"RippleState\",\n" + + " \"LowLimit\": {\n" + + " \"currency\": \"CNY\",\n" + + " \"issuer\": \"rJ1adrpGS3xsnQMb9Cw54tWJVFPuSdZHK\",\n" + + " \"value\": \"0\"\n" + + " },\n" + + " \"LowNode\": \"2\",\n" + + " \"PreviousTxnID\": \"9A5E68C795D68665A648A5A05E5BC94AA3681400353236F75139BD102D9406FD\",\n" + + " \"PreviousTxnLgrSeq\": 69746363,\n" + + " \"index\": \"6A409D31A016227B74D6A14C307239B2BBBE0CFBFCF7C271BFAF20CAA7A1E6DA\"\n" + + " },\n" + + " \"status\": \"success\"\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testCheckResult() throws JSONException, JsonProcessingException { + LedgerEntryResult result = LedgerEntryResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(83126482))) + .ledgerHash(Hash256.of("995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84")) + .validated(true) + .index(Hash256.of("56B5D2CC81461E339424869D0F5A2F4F24095B74FCD6F79960EF2D5EA10FBE00")) + .node( + CheckObject.builder() + .account(Address.of("rJk8P3yazgCSSvWXavKKCY5Y3tk4UGCiFF")) + .destination(Address.of("rHr2n1zVm5nzadgtJY5G2mUYnmWcrxfTbQ")) + .destinationNode("0") + .invoiceId(Hash256.of("5D059E085A91283DA8F2C1B8DB973994A3250ABDEDB934799A9C3EE243D3DFBD")) + .ownerNode("0") + .previousTxnId(Hash256.of("7F2DB52CA2D2C600748D7B1DF060964C74BF0219B8EF055DAB151F8A23CA1B09")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(41931458)) + .sendMax(XrpCurrencyAmount.ofDrops(12345)) + .sequence(UnsignedInteger.valueOf(41931456)) + .index(Hash256.of("56B5D2CC81461E339424869D0F5A2F4F24095B74FCD6F79960EF2D5EA10FBE00")) + .build() + ) + .status("success") + .build(); + + String json = "{\n" + + " \"ledger_hash\": \"995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84\",\n" + + " \"ledger_index\": 83126482,\n" + + " \"validated\": true,\n" + + " \"index\": \"56B5D2CC81461E339424869D0F5A2F4F24095B74FCD6F79960EF2D5EA10FBE00\",\n" + + " \"node\": {\n" + + " \"Account\": \"rJk8P3yazgCSSvWXavKKCY5Y3tk4UGCiFF\",\n" + + " \"Destination\": \"rHr2n1zVm5nzadgtJY5G2mUYnmWcrxfTbQ\",\n" + + " \"DestinationNode\": \"0\",\n" + + " \"Flags\": 0,\n" + + " \"InvoiceID\": \"5D059E085A91283DA8F2C1B8DB973994A3250ABDEDB934799A9C3EE243D3DFBD\",\n" + + " \"LedgerEntryType\": \"Check\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"PreviousTxnID\": \"7F2DB52CA2D2C600748D7B1DF060964C74BF0219B8EF055DAB151F8A23CA1B09\",\n" + + " \"PreviousTxnLgrSeq\": 41931458,\n" + + " \"SendMax\": \"12345\",\n" + + " \"Sequence\": 41931456,\n" + + " \"index\": \"56B5D2CC81461E339424869D0F5A2F4F24095B74FCD6F79960EF2D5EA10FBE00\"\n" + + " },\n" + + " \"status\": \"success\"\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testEscrowResult() throws JSONException, JsonProcessingException { + LedgerEntryResult result = LedgerEntryResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(83126482))) + .ledgerHash(Hash256.of("995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84")) + .validated(true) + .index(Hash256.of("ABC67054C15F79FEE9183B44D2E16CA06A1804E023E6A2EDB288F4976B1BFEC5")) + .node( + EscrowObject.builder() + .account(Address.of("rEWt92vANNAghT9CC83DtnDDWZcJEL5gk1")) + .amount(XrpCurrencyAmount.ofDrops(123456)) + .cancelAfter(UnsignedLong.valueOf(750277784)) + .destination(Address.of("rMuTP1PFEFMVhDYKNBLgmre5YrCzVoCYjm")) + .destinationNode("0") + .finishAfter(UnsignedLong.valueOf(750277689)) + .ownerNode("0") + .previousTransactionId(Hash256.of("466C5F96809D62385073F6BA43F5A3C217C96C4A493B7F64F6CE5B5B64278AAA")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(41931760)) + .index(Hash256.of("ABC67054C15F79FEE9183B44D2E16CA06A1804E023E6A2EDB288F4976B1BFEC5")) + .build() + ) + .status("success") + .build(); + + String json = "{\n" + + " \"ledger_hash\": \"995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84\",\n" + + " \"ledger_index\": 83126482,\n" + + " \"validated\": true,\n" + + " \"index\": \"ABC67054C15F79FEE9183B44D2E16CA06A1804E023E6A2EDB288F4976B1BFEC5\",\n" + + " \"node\": {\n" + + " \"Account\": \"rEWt92vANNAghT9CC83DtnDDWZcJEL5gk1\",\n" + + " \"Amount\": \"123456\",\n" + + " \"CancelAfter\": 750277784,\n" + + " \"Destination\": \"rMuTP1PFEFMVhDYKNBLgmre5YrCzVoCYjm\",\n" + + " \"DestinationNode\": \"0\",\n" + + " \"FinishAfter\": 750277689,\n" + + " \"Flags\": 0,\n" + + " \"LedgerEntryType\": \"Escrow\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"PreviousTxnID\": \"466C5F96809D62385073F6BA43F5A3C217C96C4A493B7F64F6CE5B5B64278AAA\",\n" + + " \"PreviousTxnLgrSeq\": 41931760,\n" + + " \"index\": \"ABC67054C15F79FEE9183B44D2E16CA06A1804E023E6A2EDB288F4976B1BFEC5\"\n" + + " },\n" + + " \"status\": \"success\"\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testPaymentChannelResult() throws JSONException, JsonProcessingException { + LedgerEntryResult result = LedgerEntryResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(83126482))) + .ledgerHash(Hash256.of("995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84")) + .validated(true) + .index(Hash256.of("7474D1ED2DE25B055AD3C8473DDD69553ACD7325BF2B15C83D54E743C576C615")) + .node( + PayChannelObject.builder() + .account(Address.of("rtqQepGRnrvaHCDyLHcc8xY7uCTnV1aRT")) + .amount(XrpCurrencyAmount.ofDrops(10000)) + .balance(XrpCurrencyAmount.ofDrops(0)) + .cancelAfter(UnsignedLong.valueOf(533171558)) + .destination(Address.of("r4sxKQshFFUvN8xDiP6KsnUKTyFi1un8UQ")) + .ownerNode("0") + .previousTransactionId(Hash256.of("987A299731B80FF14519FF28F18C14B0C55487133E086EAC895099428D57737C")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(41931835)) + .publicKey("EDAF1B0148D4FBB6BC0FCDA97C917C0BD831A654EBFD9B7D84FCB13ADE1BCB5C44") + .settleDelay(UnsignedLong.ONE) + .index(Hash256.of("7474D1ED2DE25B055AD3C8473DDD69553ACD7325BF2B15C83D54E743C576C615")) + .build() + ) + .status("success") + .build(); + + String json = "{\n" + + " \"ledger_hash\": \"995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84\",\n" + + " \"ledger_index\": 83126482,\n" + + " \"validated\": true,\n" + + " \"index\": \"7474D1ED2DE25B055AD3C8473DDD69553ACD7325BF2B15C83D54E743C576C615\",\n" + + " \"node\": {\n" + + " \"Account\": \"rtqQepGRnrvaHCDyLHcc8xY7uCTnV1aRT\",\n" + + " \"Amount\": \"10000\",\n" + + " \"Balance\": \"0\",\n" + + " \"CancelAfter\": 533171558,\n" + + " \"Destination\": \"r4sxKQshFFUvN8xDiP6KsnUKTyFi1un8UQ\",\n" + + " \"Flags\": 0,\n" + + " \"LedgerEntryType\": \"PayChannel\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"PreviousTxnID\": \"987A299731B80FF14519FF28F18C14B0C55487133E086EAC895099428D57737C\",\n" + + " \"PreviousTxnLgrSeq\": 41931835,\n" + + " \"PublicKey\": \"EDAF1B0148D4FBB6BC0FCDA97C917C0BD831A654EBFD9B7D84FCB13ADE1BCB5C44\",\n" + + " \"SettleDelay\": 1,\n" + + " \"index\": \"7474D1ED2DE25B055AD3C8473DDD69553ACD7325BF2B15C83D54E743C576C615\"\n" + + " },\n" + + " \"status\": \"success\"\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testDepositPreAuthResult() throws JSONException, JsonProcessingException { + LedgerEntryResult result = LedgerEntryResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(83126482))) + .ledgerHash(Hash256.of("995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84")) + .validated(true) + .index(Hash256.of("4CFA41F0CEB3BBECB0799BCD4E70057A80B98E762AD655D005BE90992E32CDF7")) + .node( + DepositPreAuthObject.builder() + .account(Address.of("rnmLMp1znQHpSM7xKzL1rg9unXiu1o8ptU")) + .authorize(Address.of("r4yaMT4QVKFQsyw5sLrJMETe3Wx1L5P9Pe")) + .ownerNode("0") + .previousTransactionId(Hash256.of("8D2D634EC7E5B4C6BCB5D4DD72575D42A60A11AD91E5A991692E525E6BF463BA")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(41931900)) + .index(Hash256.of("4CFA41F0CEB3BBECB0799BCD4E70057A80B98E762AD655D005BE90992E32CDF7")) + .build() + ) + .status("success") + .build(); + + String json = "{\n" + + " \"ledger_hash\": \"995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84\",\n" + + " \"ledger_index\": 83126482,\n" + + " \"validated\": true,\n" + + " \"index\": \"4CFA41F0CEB3BBECB0799BCD4E70057A80B98E762AD655D005BE90992E32CDF7\",\n" + + " \"node\": {\n" + + " \"Account\": \"rnmLMp1znQHpSM7xKzL1rg9unXiu1o8ptU\",\n" + + " \"Authorize\": \"r4yaMT4QVKFQsyw5sLrJMETe3Wx1L5P9Pe\",\n" + + " \"Flags\": 0,\n" + + " \"LedgerEntryType\": \"DepositPreauth\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"PreviousTxnID\": \"8D2D634EC7E5B4C6BCB5D4DD72575D42A60A11AD91E5A991692E525E6BF463BA\",\n" + + " \"PreviousTxnLgrSeq\": 41931900,\n" + + " \"index\": \"4CFA41F0CEB3BBECB0799BCD4E70057A80B98E762AD655D005BE90992E32CDF7\"\n" + + " },\n" + + " \"status\": \"success\"\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testTicketResult() throws JSONException, JsonProcessingException { + LedgerEntryResult result = LedgerEntryResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(83126482))) + .ledgerHash(Hash256.of("995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84")) + .validated(true) + .index(Hash256.of("8A0FB133F2D9875961990CE1F6CBB08120C7BD9B330B5D2C9718DE2A4ABCFC47")) + .node( + TicketObject.builder() + .account(Address.of("rKfyHN2fbAJuHtSc1gStGDxxbq4kf9VPFQ")) + .ownerNode("0") + .previousTransactionId(Hash256.of("AB5B87765DABF11B9FE5B40506E355532BBE3CF0ADC8C15AF1CD82F4F68CD13D")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(41932010)) + .ticketSequence(UnsignedInteger.valueOf(41932009)) + .index(Hash256.of("8A0FB133F2D9875961990CE1F6CBB08120C7BD9B330B5D2C9718DE2A4ABCFC47")) + .build() + ) + .status("success") + .build(); + + String json = "{\n" + + " \"ledger_hash\": \"995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84\",\n" + + " \"ledger_index\": 83126482,\n" + + " \"validated\": true,\n" + + " \"index\": \"8A0FB133F2D9875961990CE1F6CBB08120C7BD9B330B5D2C9718DE2A4ABCFC47\",\n" + + " \"node\": {\n" + + " \"Account\": \"rKfyHN2fbAJuHtSc1gStGDxxbq4kf9VPFQ\",\n" + + " \"Flags\": 0,\n" + + " \"LedgerEntryType\": \"Ticket\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"PreviousTxnID\": \"AB5B87765DABF11B9FE5B40506E355532BBE3CF0ADC8C15AF1CD82F4F68CD13D\",\n" + + " \"PreviousTxnLgrSeq\": 41932010,\n" + + " \"TicketSequence\": 41932009,\n" + + " \"index\": \"8A0FB133F2D9875961990CE1F6CBB08120C7BD9B330B5D2C9718DE2A4ABCFC47\"\n" + + " },\n" + + " \"status\": \"success\"\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testNftPageResult() throws JSONException, JsonProcessingException { + LedgerEntryResult result = LedgerEntryResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(83126482))) + .ledgerHash(Hash256.of("995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84")) + .validated(true) + .index(Hash256.of("4070656F661A60726DBB384E09F6E36B88071072FFFFFFFFFFFFFFFFFFFFFFFF")) + .node( + NfTokenPageObject.builder() + .addNfTokens( + NfTokenWrapper.of( + NfToken.builder() + .nfTokenId(NfTokenId.of("000000004070656F661A60726DBB384E09F6E36B880710720000099A00000000")) + .uri(NfTokenUri.of( + "697066733A2F2F62616679626569676479727A74357366703775646D376875373675683779323" + + "66E6634646675796C71616266336F636C67747179353566627A6469") + ) + .build() + ) + ) + + .previousTransactionId(Hash256.of("C94CF5BC9DF78A75997E93C71CDD8A2776E2DF279DD6F394BF4976045D960C4B")) + .previousTransactionLedgerSequence(LedgerIndex.of(UnsignedInteger.valueOf(41932089))) + .index(Hash256.of("4070656F661A60726DBB384E09F6E36B88071072FFFFFFFFFFFFFFFFFFFFFFFF")) + .build() + ) + .status("success") + .build(); + + String json = "{\n" + + " \"ledger_hash\": \"995F5C7565065ED88C251225C15A02C95D6AADD4AC75E199A9234FA8322B5F84\",\n" + + " \"ledger_index\": 83126482,\n" + + " \"validated\": true,\n" + + " \"index\": \"4070656F661A60726DBB384E09F6E36B88071072FFFFFFFFFFFFFFFFFFFFFFFF\",\n" + + " \"node\": {\n" + + " \"LedgerEntryType\": \"NFTokenPage\",\n" + + " \"NFTokens\": [\n" + + " {\n" + + " \"NFToken\": {\n" + + " \"NFTokenID\": \"000000004070656F661A60726DBB384E09F6E36B880710720000099A00000000\",\n" + + " \"URI\": \"697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377" + + "932366E6634646675796C71616266336F636C67747179353566627A6469\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"PreviousTxnID\": \"C94CF5BC9DF78A75997E93C71CDD8A2776E2DF279DD6F394BF4976045D960C4B\",\n" + + " \"PreviousTxnLgrSeq\": 41932089,\n" + + " \"index\": \"4070656F661A60726DBB384E09F6E36B88071072FFFFFFFFFFFFFFFFFFFFFFFF\"\n" + + " },\n" + + " \"status\": \"success\"\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testWithHashAndLedgerIndex() { + LedgerEntryResult result = LedgerEntryResult.builder() + .node(mock(LedgerObject.class)) + .ledgerHash(HASH_256) + .ledgerIndex(LedgerIndex.of(UnsignedInteger.ONE)) + .index(HASH_256) + .build(); + + assertThat(result.ledgerHash()).isNotEmpty().get().isEqualTo(result.ledgerHashSafe()); + assertThat(result.ledgerIndex()).isNotEmpty().get().isEqualTo(result.ledgerIndexSafe()); + assertThat(result.ledgerCurrentIndex()).isEmpty(); + assertThatThrownBy(result::ledgerCurrentIndexSafe) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Result did not contain a ledgerCurrentIndex."); + } + + @Test + void testWithLedgerCurrentIndex() { + LedgerEntryResult result = LedgerEntryResult.builder() + .node(mock(LedgerObject.class)) + .ledgerCurrentIndex(LedgerIndex.of(UnsignedInteger.ONE)) + .index(HASH_256) + .build(); + + assertThat(result.ledgerCurrentIndex()).isNotEmpty().get().isEqualTo(result.ledgerCurrentIndexSafe()); + assertThat(result.ledgerHash()).isEmpty(); + assertThat(result.ledgerIndex()).isEmpty(); + assertThatThrownBy(result::ledgerHashSafe) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Result did not contain a ledgerHash."); + assertThatThrownBy(result::ledgerIndexSafe) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Result did not contain a ledgerIndex."); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AccountRootFlagsTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AccountRootFlagsTests.java index c8b4ed840..e5f7e7668 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AccountRootFlagsTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AccountRootFlagsTests.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. @@ -34,7 +34,7 @@ public class AccountRootFlagsTests extends AbstractFlagsTest { public static Stream data() { - return getBooleanCombinations(13); + return getBooleanCombinations(14); } @ParameterizedTest @@ -53,7 +53,8 @@ public void testDeriveIndividualFlagsFromFlags( boolean lsfDisallowIncomingNFTokenOffer, boolean lsfDisallowIncomingCheck, boolean lsfDisallowIncomingPayChan, - boolean lsfDisallowIncomingTrustline + boolean lsfDisallowIncomingTrustline, + boolean lsfAllowTrustlineClawback ) { long expectedFlags = (lsfDefaultRipple ? AccountRootFlags.DEFAULT_RIPPLE.getValue() : 0L) | (lsfDepositAuth ? AccountRootFlags.DEPOSIT_AUTH.getValue() : 0L) | @@ -67,7 +68,8 @@ public void testDeriveIndividualFlagsFromFlags( (lsfDisallowIncomingNFTokenOffer ? AccountRootFlags.DISALLOW_INCOMING_NFT_OFFER.getValue() : 0L) | (lsfDisallowIncomingCheck ? AccountRootFlags.DISALLOW_INCOMING_CHECK.getValue() : 0L) | (lsfDisallowIncomingPayChan ? AccountRootFlags.DISALLOW_INCOMING_PAY_CHAN.getValue() : 0L) | - (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE.getValue() : 0L); + (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE.getValue() : 0L) | + (lsfAllowTrustlineClawback ? AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK.getValue() : 0L); Flags flagsFromFlags = AccountRootFlags.of( (lsfDefaultRipple ? AccountRootFlags.DEFAULT_RIPPLE : AccountRootFlags.UNSET), (lsfDepositAuth ? AccountRootFlags.DEPOSIT_AUTH : AccountRootFlags.UNSET), @@ -81,7 +83,8 @@ public void testDeriveIndividualFlagsFromFlags( (lsfDisallowIncomingNFTokenOffer ? AccountRootFlags.DISALLOW_INCOMING_NFT_OFFER : AccountRootFlags.UNSET), (lsfDisallowIncomingCheck ? AccountRootFlags.DISALLOW_INCOMING_CHECK : AccountRootFlags.UNSET), (lsfDisallowIncomingPayChan ? AccountRootFlags.DISALLOW_INCOMING_PAY_CHAN : AccountRootFlags.UNSET), - (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE : AccountRootFlags.UNSET) + (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE : AccountRootFlags.UNSET), + (lsfAllowTrustlineClawback ? AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK : AccountRootFlags.UNSET) ); assertThat(flagsFromFlags.getValue()).isEqualTo(expectedFlags); @@ -102,6 +105,7 @@ public void testDeriveIndividualFlagsFromFlags( assertThat(flagsFromLong.lsfDisallowIncomingCheck()).isEqualTo(lsfDisallowIncomingCheck); assertThat(flagsFromLong.lsfDisallowIncomingPayChan()).isEqualTo(lsfDisallowIncomingPayChan); assertThat(flagsFromLong.lsfDisallowIncomingTrustline()).isEqualTo(lsfDisallowIncomingTrustline); + assertThat(flagsFromLong.lsfAllowTrustLineClawback()).isEqualTo(lsfAllowTrustlineClawback); } @ParameterizedTest @@ -120,7 +124,8 @@ void testJson( boolean lsfDisallowIncomingNFTokenOffer, boolean lsfDisallowIncomingCheck, boolean lsfDisallowIncomingPayChan, - boolean lsfDisallowIncomingTrustline + boolean lsfDisallowIncomingTrustline, + boolean lsfAllowTrustlineClawback ) throws JSONException, JsonProcessingException { Flags flags = AccountRootFlags.of( (lsfDefaultRipple ? AccountRootFlags.DEFAULT_RIPPLE : AccountRootFlags.UNSET), @@ -135,7 +140,8 @@ void testJson( (lsfDisallowIncomingNFTokenOffer ? AccountRootFlags.DISALLOW_INCOMING_NFT_OFFER : AccountRootFlags.UNSET), (lsfDisallowIncomingCheck ? AccountRootFlags.DISALLOW_INCOMING_CHECK : AccountRootFlags.UNSET), (lsfDisallowIncomingPayChan ? AccountRootFlags.DISALLOW_INCOMING_PAY_CHAN : AccountRootFlags.UNSET), - (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE : AccountRootFlags.UNSET) + (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE : AccountRootFlags.UNSET), + (lsfAllowTrustlineClawback ? AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK : AccountRootFlags.UNSET) ); FlagsWrapper flagsWrapper = FlagsWrapper.of(flags); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AmmDepositFlagsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AmmDepositFlagsTest.java new file mode 100644 index 000000000..f011c82a4 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AmmDepositFlagsTest.java @@ -0,0 +1,59 @@ +package org.xrpl.xrpl4j.model.flags; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.jupiter.api.Test; + +public class AmmDepositFlagsTest { + + @Test + void testFlagValues() { + AmmDepositFlags lpToken = AmmDepositFlags.LP_TOKEN; + assertThat(lpToken.tfLpToken()).isTrue(); + assertThat(lpToken.tfSingleAsset()).isFalse(); + assertThat(lpToken.tfTwoAsset()).isFalse(); + assertThat(lpToken.tfOneAssetLpToken()).isFalse(); + assertThat(lpToken.tfLimitLpToken()).isFalse(); + assertThat(lpToken.tfTwoAssetIfEmpty()).isFalse(); + + AmmDepositFlags singleAsset = AmmDepositFlags.SINGLE_ASSET; + assertThat(singleAsset.tfLpToken()).isFalse(); + assertThat(singleAsset.tfSingleAsset()).isTrue(); + assertThat(singleAsset.tfTwoAsset()).isFalse(); + assertThat(singleAsset.tfOneAssetLpToken()).isFalse(); + assertThat(singleAsset.tfLimitLpToken()).isFalse(); + assertThat(singleAsset.tfTwoAssetIfEmpty()).isFalse(); + + AmmDepositFlags twoAsset = AmmDepositFlags.TWO_ASSET; + assertThat(twoAsset.tfLpToken()).isFalse(); + assertThat(twoAsset.tfSingleAsset()).isFalse(); + assertThat(twoAsset.tfTwoAsset()).isTrue(); + assertThat(twoAsset.tfOneAssetLpToken()).isFalse(); + assertThat(twoAsset.tfLimitLpToken()).isFalse(); + assertThat(twoAsset.tfTwoAssetIfEmpty()).isFalse(); + + AmmDepositFlags oneAssetLpToken = AmmDepositFlags.ONE_ASSET_LP_TOKEN; + assertThat(oneAssetLpToken.tfLpToken()).isFalse(); + assertThat(oneAssetLpToken.tfSingleAsset()).isFalse(); + assertThat(oneAssetLpToken.tfTwoAsset()).isFalse(); + assertThat(oneAssetLpToken.tfOneAssetLpToken()).isTrue(); + assertThat(oneAssetLpToken.tfLimitLpToken()).isFalse(); + assertThat(oneAssetLpToken.tfTwoAssetIfEmpty()).isFalse(); + + AmmDepositFlags limitLpToken = AmmDepositFlags.LIMIT_LP_TOKEN; + assertThat(limitLpToken.tfLpToken()).isFalse(); + assertThat(limitLpToken.tfSingleAsset()).isFalse(); + assertThat(limitLpToken.tfTwoAsset()).isFalse(); + assertThat(limitLpToken.tfOneAssetLpToken()).isFalse(); + assertThat(limitLpToken.tfLimitLpToken()).isTrue(); + assertThat(limitLpToken.tfTwoAssetIfEmpty()).isFalse(); + + AmmDepositFlags twoAssetIfEmpty = AmmDepositFlags.TWO_ASSET_IF_EMPTY; + assertThat(twoAssetIfEmpty.tfLpToken()).isFalse(); + assertThat(twoAssetIfEmpty.tfSingleAsset()).isFalse(); + assertThat(twoAssetIfEmpty.tfTwoAsset()).isFalse(); + assertThat(twoAssetIfEmpty.tfOneAssetLpToken()).isFalse(); + assertThat(twoAssetIfEmpty.tfLimitLpToken()).isFalse(); + assertThat(twoAssetIfEmpty.tfTwoAssetIfEmpty()).isTrue(); + } +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AmmWithdrawFlagsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AmmWithdrawFlagsTest.java new file mode 100644 index 000000000..ba89e32ba --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AmmWithdrawFlagsTest.java @@ -0,0 +1,75 @@ +package org.xrpl.xrpl4j.model.flags; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.jupiter.api.Test; + +public class AmmWithdrawFlagsTest { + + @Test + void testFlagValues() { + AmmWithdrawFlags lpToken = AmmWithdrawFlags.LP_TOKEN; + assertThat(lpToken.tfLpToken()).isTrue(); + assertThat(lpToken.tfWithdrawAll()).isFalse(); + assertThat(lpToken.tfOneAssetWithdrawAll()).isFalse(); + assertThat(lpToken.tfSingleAsset()).isFalse(); + assertThat(lpToken.tfTwoAsset()).isFalse(); + assertThat(lpToken.tfOneAssetLpToken()).isFalse(); + assertThat(lpToken.tfLimitLpToken()).isFalse(); + + AmmWithdrawFlags withdrawAll = AmmWithdrawFlags.WITHDRAW_ALL; + assertThat(withdrawAll.tfLpToken()).isFalse(); + assertThat(withdrawAll.tfWithdrawAll()).isTrue(); + assertThat(withdrawAll.tfOneAssetWithdrawAll()).isFalse(); + assertThat(withdrawAll.tfSingleAsset()).isFalse(); + assertThat(withdrawAll.tfTwoAsset()).isFalse(); + assertThat(withdrawAll.tfOneAssetLpToken()).isFalse(); + assertThat(withdrawAll.tfLimitLpToken()).isFalse(); + + AmmWithdrawFlags oneAssetWithdrawAll = AmmWithdrawFlags.ONE_ASSET_WITHDRAW_ALL; + assertThat(oneAssetWithdrawAll.tfLpToken()).isFalse(); + assertThat(oneAssetWithdrawAll.tfWithdrawAll()).isFalse(); + assertThat(oneAssetWithdrawAll.tfOneAssetWithdrawAll()).isTrue(); + assertThat(oneAssetWithdrawAll.tfSingleAsset()).isFalse(); + assertThat(oneAssetWithdrawAll.tfTwoAsset()).isFalse(); + assertThat(oneAssetWithdrawAll.tfOneAssetLpToken()).isFalse(); + assertThat(oneAssetWithdrawAll.tfLimitLpToken()).isFalse(); + + AmmWithdrawFlags singleAsset = AmmWithdrawFlags.SINGLE_ASSET; + assertThat(singleAsset.tfLpToken()).isFalse(); + assertThat(singleAsset.tfWithdrawAll()).isFalse(); + assertThat(singleAsset.tfOneAssetWithdrawAll()).isFalse(); + assertThat(singleAsset.tfSingleAsset()).isTrue(); + assertThat(singleAsset.tfTwoAsset()).isFalse(); + assertThat(singleAsset.tfOneAssetLpToken()).isFalse(); + assertThat(singleAsset.tfLimitLpToken()).isFalse(); + + AmmWithdrawFlags twoAsset = AmmWithdrawFlags.TWO_ASSET; + assertThat(twoAsset.tfLpToken()).isFalse(); + assertThat(twoAsset.tfWithdrawAll()).isFalse(); + assertThat(twoAsset.tfOneAssetWithdrawAll()).isFalse(); + assertThat(twoAsset.tfSingleAsset()).isFalse(); + assertThat(twoAsset.tfTwoAsset()).isTrue(); + assertThat(twoAsset.tfOneAssetLpToken()).isFalse(); + assertThat(twoAsset.tfLimitLpToken()).isFalse(); + + AmmWithdrawFlags oneAssetLpToken = AmmWithdrawFlags.ONE_ASSET_LP_TOKEN; + assertThat(oneAssetLpToken.tfLpToken()).isFalse(); + assertThat(oneAssetLpToken.tfWithdrawAll()).isFalse(); + assertThat(oneAssetLpToken.tfOneAssetWithdrawAll()).isFalse(); + assertThat(oneAssetLpToken.tfSingleAsset()).isFalse(); + assertThat(oneAssetLpToken.tfTwoAsset()).isFalse(); + assertThat(oneAssetLpToken.tfOneAssetLpToken()).isTrue(); + assertThat(oneAssetLpToken.tfLimitLpToken()).isFalse(); + + AmmWithdrawFlags limitLpToken = AmmWithdrawFlags.LIMIT_LP_TOKEN; + assertThat(limitLpToken.tfLpToken()).isFalse(); + assertThat(limitLpToken.tfWithdrawAll()).isFalse(); + assertThat(limitLpToken.tfOneAssetWithdrawAll()).isFalse(); + assertThat(limitLpToken.tfSingleAsset()).isFalse(); + assertThat(limitLpToken.tfTwoAsset()).isFalse(); + assertThat(limitLpToken.tfOneAssetLpToken()).isFalse(); + assertThat(limitLpToken.tfLimitLpToken()).isTrue(); + + } +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/AmmObjectTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/AmmObjectTest.java new file mode 100644 index 000000000..ec28b7e23 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/AmmObjectTest.java @@ -0,0 +1,125 @@ +package org.xrpl.xrpl4j.model.ledger; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.xrpl.xrpl4j.crypto.TestConstants.HASH_256; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TradingFee; +import org.xrpl.xrpl4j.model.transactions.VoteWeight; + +class AmmObjectTest extends AbstractJsonTest { + + @Test + void voteSlotsUnwrapped() { + VoteEntry voteEntry1 = VoteEntry.builder() + .account(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .voteWeight(VoteWeight.of(UnsignedInteger.ONE)) + .tradingFee(TradingFee.of(UnsignedInteger.ONE)) + .build(); + VoteEntry voteEntry2 = VoteEntry.builder() + .account(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .voteWeight(VoteWeight.of(UnsignedInteger.valueOf(2))) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(2))) + .build(); + AmmObject ammObject = AmmObject.builder() + .asset(mock(Issue.class)) + .asset2(mock(Issue.class)) + .account(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .lpTokenBalance(mock(IssuedCurrencyAmount.class)) + .tradingFee(TradingFee.of(UnsignedInteger.ONE)) + .addVoteSlots( + VoteEntryWrapper.of(voteEntry1), + VoteEntryWrapper.of(voteEntry2) + ) + .index(HASH_256) + .ownerNode("0") + .build(); + + assertThat(ammObject.voteSlotsUnwrapped()).asList() + .containsExactlyInAnyOrder(voteEntry1, voteEntry2); + } + + @Test + void testJson() throws JSONException, JsonProcessingException { + AmmObject ammObject = AmmObject.builder() + .asset(Issue.XRP) + .asset2( + Issue.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .build() + ) + .account(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .lpTokenBalance( + IssuedCurrencyAmount.builder() + .value("71150.53584131501") + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .build() + ) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(600))) + .addVoteSlots( + VoteEntryWrapper.of( + VoteEntry.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .voteWeight(VoteWeight.of(UnsignedInteger.valueOf(100000))) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(600))) + .build() + ) + ) + .auctionSlot( + AuctionSlot.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .addAuthAccounts( + AuthAccountWrapper.of( + AuthAccount.of( + Address.of("rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg") + ) + ), + AuthAccountWrapper.of( + AuthAccount.of( + Address.of("rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv") + ) + ) + ) + .discountedFee(TradingFee.of(UnsignedInteger.ZERO)) + .price( + IssuedCurrencyAmount.builder() + .value("0.8696263565463045") + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .build() + ) + .expiration(UnsignedInteger.valueOf(721870180)) + .build() + ) + .index(HASH_256) + .ownerNode("0") + .build(); + + String json = String.format("{\n" + + " \"Account\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"LedgerEntryType\" : \"AMM\",\n" + + " \"Asset\" : " + objectMapper.writeValueAsString(ammObject.asset()) + "," + + " \"Asset2\" : " + objectMapper.writeValueAsString(ammObject.asset2()) + "," + + " \"AuctionSlot\" : " + objectMapper.writeValueAsString(ammObject.auctionSlot()) + "," + + " \"Flags\" : 0,\n" + + " \"LPTokenBalance\" : " + objectMapper.writeValueAsString(ammObject.lpTokenBalance()) + "," + + " \"TradingFee\" : 600,\n" + + " \"index\" : %s,\n" + + " \"OwnerNode\" : \"0\",\n" + + " \"VoteSlots\" : [\n" + + objectMapper.writeValueAsString(ammObject.voteSlots().get(0)) + + " ]\n" + + "}", HASH_256); + + assertCanSerializeAndDeserialize(ammObject, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/AuctionSlotTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/AuctionSlotTest.java new file mode 100644 index 000000000..bb6d9bcb6 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/AuctionSlotTest.java @@ -0,0 +1,78 @@ +package org.xrpl.xrpl4j.model.ledger; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TradingFee; + +class AuctionSlotTest extends AbstractJsonTest { + + @Test + void authAccountsAddresses() { + Address address1 = Address.of("rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn"); + Address address2 = Address.of("rB1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn"); + AuctionSlot slot = AuctionSlot.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .discountedFee(TradingFee.of(UnsignedInteger.ONE)) + .price(mock(IssuedCurrencyAmount.class)) + .expiration(UnsignedInteger.ONE) + .addAuthAccounts( + AuthAccountWrapper.of(AuthAccount.of(address1)), + AuthAccountWrapper.of(AuthAccount.of(address2)) + ) + .build(); + assertThat(slot.authAccountsAddresses()).asList().containsExactlyInAnyOrder(address1, address2); + } + + @Test + void testJson() throws JSONException, JsonProcessingException { + AuctionSlot slot = AuctionSlot.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .discountedFee(TradingFee.of(UnsignedInteger.ZERO)) + .price( + IssuedCurrencyAmount.builder() + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .value("0.8696263565463045") + .build() + ) + .expiration(UnsignedInteger.valueOf(721870180)) + .addAuthAccounts( + AuthAccountWrapper.of(AuthAccount.of(Address.of("rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg"))), + AuthAccountWrapper.of(AuthAccount.of(Address.of("rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv"))) + ) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"AuthAccounts\" : [\n" + + " {\n" + + " \"AuthAccount\" : {\n" + + " \"Account\" : \"rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"AuthAccount\" : {\n" + + " \"Account\" : \"rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"DiscountedFee\" : 0,\n" + + " \"Expiration\" : 721870180,\n" + + " \"Price\" : {\n" + + " \"currency\" : \"039C99CD9AB0B70B32ECDA51EAAE471625608EA2\",\n" + + " \"issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"value\" : \"0.8696263565463045\"\n" + + " }\n" + + " }"; + + assertCanSerializeAndDeserialize(slot, json, AuctionSlot.class); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/TicketObjectJsonTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/TicketObjectJsonTests.java index 3d2ba457e..3fb25e4a2 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/TicketObjectJsonTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/TicketObjectJsonTests.java @@ -20,6 +20,8 @@ * =========================LICENSE_END================================== */ +import static org.xrpl.xrpl4j.crypto.TestConstants.HASH_256; + import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.primitives.UnsignedInteger; import org.json.JSONException; @@ -38,17 +40,19 @@ void testJson() throws JSONException, JsonProcessingException { .previousTransactionId(Hash256.of("F19AD4577212D3BEACA0F75FE1BA1644F2E854D46E8D62E9C95D18E9708CBFB1")) .previousTransactionLedgerSequence(UnsignedInteger.valueOf(4)) .ticketSequence(UnsignedInteger.valueOf(3)) + .index(HASH_256) .build(); - String json = "{\n" + + String json = String.format("{\n" + " \"Account\" : \"rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de\",\n" + " \"Flags\" : 0,\n" + " \"LedgerEntryType\" : \"Ticket\",\n" + " \"OwnerNode\" : \"0000000000000000\",\n" + " \"PreviousTxnID\" : \"F19AD4577212D3BEACA0F75FE1BA1644F2E854D46E8D62E9C95D18E9708CBFB1\",\n" + " \"PreviousTxnLgrSeq\" : 4,\n" + + " \"index\" : %s,\n" + " \"TicketSequence\" : 3\n" + - "}"; + "}", HASH_256); assertCanSerializeAndDeserialize(ticketObject, json); } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/VoteEntryWrapperTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/VoteEntryWrapperTest.java new file mode 100644 index 000000000..89ddcb4f6 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/VoteEntryWrapperTest.java @@ -0,0 +1,33 @@ +package org.xrpl.xrpl4j.model.ledger; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.TradingFee; +import org.xrpl.xrpl4j.model.transactions.VoteWeight; + +class VoteEntryWrapperTest extends AbstractJsonTest { + + @Test + void testJson() throws JSONException, JsonProcessingException { + VoteEntryWrapper voteWrapper = VoteEntryWrapper.of( + VoteEntry.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(600))) + .voteWeight(VoteWeight.of(UnsignedInteger.valueOf(100000))) + .build() + ); + String json = "{\n" + + " \"VoteEntry\" : {\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"TradingFee\" : 600,\n" + + " \"VoteWeight\" : 100000\n" + + " }\n" + + " }"; + + assertCanSerializeAndDeserialize(voteWrapper, json, VoteEntryWrapper.class); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AccountSetTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AccountSetTests.java index cdf4cb094..5eb5a4479 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AccountSetTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AccountSetTests.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. @@ -21,15 +21,27 @@ */ 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.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.xrpl.xrpl4j.model.flags.AccountSetTransactionFlags; +import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag; + +import java.util.Arrays; +import java.util.stream.Stream; public class AccountSetTests { + public static Stream accountSetFlags() { + return Arrays.stream(AccountSetFlag.values()).map(Arguments::of); + } + @Test public void simpleAccountSet() { AccountSet accountSet = AccountSet.builder() @@ -49,12 +61,209 @@ public void simpleAccountSet() { assertThat(accountSet.sequence()).isEqualTo(UnsignedInteger.valueOf(5)); assertThat(accountSet.domain()).isNotEmpty().get().isEqualTo("6578616D706C652E636F6D"); assertThat(accountSet.setFlag()).isNotEmpty().get().isEqualTo(AccountSet.AccountSetFlag.ACCOUNT_TXN_ID); + assertThat(accountSet.setFlagRawValue()).isNotEmpty().get() + .isEqualTo(UnsignedInteger.valueOf(AccountSetFlag.ACCOUNT_TXN_ID.getValue())); assertThat(accountSet.messageKey()).isNotEmpty().get() .isEqualTo("03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB"); assertThat(accountSet.transferRate()).isNotEmpty().get().isEqualTo(UnsignedInteger.valueOf(1000000001)); assertThat(accountSet.flags().isEmpty()).isTrue(); } + @Test + void testWithEmptyClearFlagAndEmptyRawValue() { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .build(); + + assertThat(accountSet.clearFlag()).isEmpty(); + assertThat(accountSet.clearFlagRawValue()).isEmpty(); + } + + @ParameterizedTest + @MethodSource("accountSetFlags") + void testWithPresentClearFlagAndPresentRawValue(AccountSetFlag accountSetFlag) { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .clearFlag(accountSetFlag) + .clearFlagRawValue(UnsignedInteger.valueOf(accountSetFlag.getValue())) + .build(); + + assertThat(accountSet.clearFlag()).isNotEmpty().get().isEqualTo(accountSetFlag); + assertThat(accountSet.clearFlagRawValue()).isNotEmpty().get() + .isEqualTo(UnsignedInteger.valueOf(accountSetFlag.getValue())); + } + + @ParameterizedTest + @MethodSource("accountSetFlags") + void testWithPresentClearFlagAndPresentRawValueThrowsForMismatchedValues(AccountSetFlag accountSetFlag) { + assertThatThrownBy( + () -> AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .clearFlag(accountSetFlag) + .clearFlagRawValue(UnsignedInteger.valueOf(accountSetFlag.getValue()).plus(UnsignedInteger.ONE)) + .build() + ).isInstanceOf(IllegalStateException.class) + .hasMessage(String.format("clearFlag and clearFlagRawValue should be equivalent, but clearFlag's underlying " + + "value was %s and clearFlagRawValue was %s", + accountSetFlag.getValue(), + accountSetFlag.getValue() + 1 + )); + } + + @ParameterizedTest + @MethodSource("accountSetFlags") + void testWithPresentClearFlagAndEmptyRawValue(AccountSetFlag accountSetFlag) { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .clearFlag(accountSetFlag) + .build(); + + assertThat(accountSet.clearFlag()).isNotEmpty().get().isEqualTo(accountSetFlag); + assertThat(accountSet.clearFlagRawValue()).isNotEmpty().get() + .isEqualTo(UnsignedInteger.valueOf(accountSetFlag.getValue())); + } + + @ParameterizedTest + @MethodSource("accountSetFlags") + void testWithEmptyClearFlagAndPresentRawValue(AccountSetFlag accountSetFlag) { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .clearFlagRawValue(UnsignedInteger.valueOf(accountSetFlag.getValue())) + .build(); + + assertThat(accountSet.clearFlag()).isNotEmpty().get().isEqualTo(accountSetFlag); + assertThat(accountSet.clearFlagRawValue()).isNotEmpty().get() + .isEqualTo(UnsignedInteger.valueOf(accountSetFlag.getValue())); + } + + @Test + void testWithEmptyClearFlagAndPresentInvalidRawValue() { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .clearFlagRawValue(UnsignedInteger.valueOf(AccountSetFlag.MAX_VALUE + 1)) + .build(); + + assertThat(accountSet.clearFlag()).isEmpty(); + assertThat(accountSet.clearFlagRawValue()).isNotEmpty().get() + .isEqualTo(UnsignedInteger.valueOf(AccountSetFlag.MAX_VALUE + 1)); + } + + ////////////////////////////////////// + + @Test + void testWithEmptySetFlagAndEmptyRawValue() { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .build(); + + assertThat(accountSet.setFlag()).isEmpty(); + assertThat(accountSet.setFlagRawValue()).isEmpty(); + } + + @ParameterizedTest + @MethodSource("accountSetFlags") + void testWithPresentSetFlagAndPresentRawValue(AccountSetFlag accountSetFlag) { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .setFlag(accountSetFlag) + .setFlagRawValue(UnsignedInteger.valueOf(accountSetFlag.getValue())) + .build(); + + assertThat(accountSet.setFlag()).isNotEmpty().get().isEqualTo(accountSetFlag); + assertThat(accountSet.setFlagRawValue()).isNotEmpty().get() + .isEqualTo(UnsignedInteger.valueOf(accountSetFlag.getValue())); + } + + @ParameterizedTest + @MethodSource("accountSetFlags") + void testWithPresentSetFlagAndPresentRawValueThrowsForMismatchedValues(AccountSetFlag accountSetFlag) { + assertThatThrownBy( + () -> AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .setFlag(accountSetFlag) + .setFlagRawValue(UnsignedInteger.valueOf(accountSetFlag.getValue()).plus(UnsignedInteger.ONE)) + .build() + ).isInstanceOf(IllegalStateException.class) + .hasMessage(String.format("setFlag and setFlagRawValue should be equivalent, but setFlag's underlying " + + "value was %s and setFlagRawValue was %s", + accountSetFlag.getValue(), + accountSetFlag.getValue() + 1 + )); + } + + @ParameterizedTest + @MethodSource("accountSetFlags") + void testWithPresentSetFlagAndEmptyRawValue(AccountSetFlag accountSetFlag) { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .setFlag(accountSetFlag) + .build(); + + assertThat(accountSet.setFlag()).isNotEmpty().get().isEqualTo(accountSetFlag); + assertThat(accountSet.setFlagRawValue()).isNotEmpty().get() + .isEqualTo(UnsignedInteger.valueOf(accountSetFlag.getValue())); + } + + @ParameterizedTest + @MethodSource("accountSetFlags") + void testWithEmptySetFlagAndPresentRawValue(AccountSetFlag accountSetFlag) { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .setFlagRawValue(UnsignedInteger.valueOf(accountSetFlag.getValue())) + .build(); + + assertThat(accountSet.setFlag()).isNotEmpty().get().isEqualTo(accountSetFlag); + assertThat(accountSet.setFlagRawValue()).isNotEmpty().get() + .isEqualTo(UnsignedInteger.valueOf(accountSetFlag.getValue())); + } + + @Test + void testWithEmptySetFlagAndPresentInvalidRawValue() { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .setFlagRawValue(UnsignedInteger.valueOf(AccountSetFlag.MAX_VALUE + 1)) + .build(); + + assertThat(accountSet.setFlag()).isEmpty(); + assertThat(accountSet.setFlagRawValue()).isNotEmpty().get() + .isEqualTo(UnsignedInteger.valueOf(AccountSetFlag.MAX_VALUE + 1)); + + accountSet = AccountSet.builder() + .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(5)) + .setFlagRawValue(UnsignedInteger.MAX_VALUE.minus(UnsignedInteger.ONE)) + .build(); + + assertThat(accountSet.setFlag()).isEmpty(); + assertThat(accountSet.setFlagRawValue()).isNotEmpty().get() + .isEqualTo(UnsignedInteger.MAX_VALUE.minus(UnsignedInteger.ONE)); + } + @Test void accountSetWithSetFlagAndTransactionFlags() { AccountSetTransactionFlags flags = AccountSetTransactionFlags.builder() diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmBidTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmBidTest.java new file mode 100644 index 000000000..91f51139b --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmBidTest.java @@ -0,0 +1,252 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; +import org.xrpl.xrpl4j.model.ledger.AuthAccount; +import org.xrpl.xrpl4j.model.ledger.AuthAccountWrapper; +import org.xrpl.xrpl4j.model.ledger.Issue; + +class AmmBidTest extends AbstractJsonTest { + + @Test + void testJsonWithoutMinAndMax() throws JSONException, JsonProcessingException { + AmmBid bid = AmmBid.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .addAuthAccounts( + AuthAccountWrapper.of(AuthAccount.of(Address.of("rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg"))), + AuthAccountWrapper.of(AuthAccount.of(Address.of("rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv"))) + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(9)) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"AuthAccounts\" : [\n" + + " {\n" + + " \"AuthAccount\" : {\n" + + " \"Account\" : \"rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"AuthAccount\" : {\n" + + " \"Account\" : \"rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"Fee\" : \"10\",\n" + + " \"Sequence\" : 9,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMBid\"\n" + + "}"; + + assertCanSerializeAndDeserialize(bid, json); + } + + @Test + void testJsonWithUnsetFlags() throws JSONException, JsonProcessingException { + AmmBid bid = AmmBid.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .addAuthAccounts( + AuthAccountWrapper.of(AuthAccount.of(Address.of("rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg"))), + AuthAccountWrapper.of(AuthAccount.of(Address.of("rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv"))) + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(9)) + .flags(TransactionFlags.UNSET) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"AuthAccounts\" : [\n" + + " {\n" + + " \"AuthAccount\" : {\n" + + " \"Account\" : \"rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"AuthAccount\" : {\n" + + " \"Account\" : \"rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : 0,\n" + + " \"Sequence\" : 9,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMBid\"\n" + + "}"; + + assertCanSerializeAndDeserialize(bid, json); + } + + @Test + void testJsonWithNonZeroFlags() throws JSONException, JsonProcessingException { + AmmBid bid = AmmBid.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .addAuthAccounts( + AuthAccountWrapper.of(AuthAccount.of(Address.of("rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg"))), + AuthAccountWrapper.of(AuthAccount.of(Address.of("rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv"))) + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(9)) + .flags(TransactionFlags.FULLY_CANONICAL_SIG) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"AuthAccounts\" : [\n" + + " {\n" + + " \"AuthAccount\" : {\n" + + " \"Account\" : \"rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"AuthAccount\" : {\n" + + " \"Account\" : \"rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : 0,\n" + + " \"Sequence\" : 9,\n" + + " \"Flags\" : 2147483648,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMBid\"\n" + + "}"; + + assertCanSerializeAndDeserialize(bid, json); + } + + @Test + void testJsonWithMinAndMax() throws JSONException, JsonProcessingException { + AmmBid bid = AmmBid.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .addAuthAccounts( + AuthAccountWrapper.of(AuthAccount.of(Address.of("rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg"))), + AuthAccountWrapper.of(AuthAccount.of(Address.of("rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv"))) + ) + .bidMax( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ) + .bidMin( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(9)) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"AuthAccounts\" : [\n" + + " {\n" + + " \"AuthAccount\" : {\n" + + " \"Account\" : \"rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"AuthAccount\" : {\n" + + " \"Account\" : \"rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"BidMax\" : {\n" + + " \"currency\" : \"039C99CD9AB0B70B32ECDA51EAAE471625608EA2\",\n" + + " \"issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"value\" : \"100\"\n" + + " },\n" + + " \"BidMin\" : {\n" + + " \"currency\" : \"039C99CD9AB0B70B32ECDA51EAAE471625608EA2\",\n" + + " \"issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"value\" : \"100\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Sequence\" : 9,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMBid\"\n" + + "}"; + + assertCanSerializeAndDeserialize(bid, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmCreateTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmCreateTest.java new file mode 100644 index 000000000..275582f17 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmCreateTest.java @@ -0,0 +1,130 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; + +class AmmCreateTest extends AbstractJsonTest { + + @Test + void testJson() throws JSONException, JsonProcessingException { + AmmCreate ammCreate = AmmCreate.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .amount( + IssuedCurrencyAmount.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .value("25") + .build() + ) + .amount2(XrpCurrencyAmount.ofDrops(250000000)) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(6)) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(500))) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\",\n" + + " \"value\" : \"25\"\n" + + " },\n" + + " \"Amount2\" : \"250000000\",\n" + + " \"Fee\" : \"10\",\n" + + " \"Sequence\" : 6,\n" + + " \"TradingFee\" : 500,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMCreate\"\n" + + "}"; + + assertCanSerializeAndDeserialize(ammCreate, json); + } + + @Test + void testJsonWithUnsetFlags() throws JSONException, JsonProcessingException { + AmmCreate ammCreate = AmmCreate.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .amount( + IssuedCurrencyAmount.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .value("25") + .build() + ) + .amount2(XrpCurrencyAmount.ofDrops(250000000)) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(6)) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(500))) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .flags(TransactionFlags.UNSET) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\",\n" + + " \"value\" : \"25\"\n" + + " },\n" + + " \"Amount2\" : \"250000000\",\n" + + " \"Fee\" : \"10\",\n" + + " \"Sequence\" : 6,\n" + + " \"TradingFee\" : 500,\n" + + " \"Flags\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMCreate\"\n" + + "}"; + + assertCanSerializeAndDeserialize(ammCreate, json); + } + + @Test + void testJsonWithNonZeroFlags() throws JSONException, JsonProcessingException { + AmmCreate ammCreate = AmmCreate.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .amount( + IssuedCurrencyAmount.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .value("25") + .build() + ) + .amount2(XrpCurrencyAmount.ofDrops(250000000)) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(6)) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(500))) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .flags(TransactionFlags.FULLY_CANONICAL_SIG) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\",\n" + + " \"value\" : \"25\"\n" + + " },\n" + + " \"Amount2\" : \"250000000\",\n" + + " \"Fee\" : \"10\",\n" + + " \"Sequence\" : 6,\n" + + " \"TradingFee\" : 500,\n" + + " \"Flags\" : 2147483648,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMCreate\"\n" + + "}"; + + assertCanSerializeAndDeserialize(ammCreate, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmDeleteTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmDeleteTest.java new file mode 100644 index 000000000..2889e572e --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmDeleteTest.java @@ -0,0 +1,49 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; +import org.xrpl.xrpl4j.model.ledger.Issue; + +class AmmDeleteTest extends AbstractJsonTest { + + @Test + void testJson() throws JSONException, JsonProcessingException { + AmmDelete ammDelete = AmmDelete.builder() + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(9)) + .signingPublicKey(PublicKey.fromBase16EncodedPublicKey( + "EDD299D60BCE7980F6082945B5597FFFD35223F1950673BFA4D4AED6FDE5097156" + )) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Sequence\" : 9,\n" + + " \"SigningPubKey\" : \"EDD299D60BCE7980F6082945B5597FFFD35223F1950673BFA4D4AED6FDE5097156\",\n" + + " \"TransactionType\" : \"AMMDelete\"\n" + + "}"; + + assertCanSerializeAndDeserialize(ammDelete, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmDepositTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmDepositTest.java new file mode 100644 index 000000000..9f4c5c1f9 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmDepositTest.java @@ -0,0 +1,347 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.AmmDepositFlags; +import org.xrpl.xrpl4j.model.ledger.Issue; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +class AmmDepositTest extends AbstractJsonTest { + + @Test + void constructLpTokenDepositAndTestJson() throws JSONException, JsonProcessingException { + AmmDeposit deposit = AmmDeposit.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .flags(AmmDepositFlags.LP_TOKEN) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .lpTokenOut( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ).build(); + + assertThat(deposit.flags()).isEqualTo(AmmDepositFlags.LP_TOKEN); + + String json = "{\n" + + " \"Account\" : \"" + deposit.account() + "\",\n" + + " \"LPTokenOut\" : {\n" + + " \"currency\" : \"039C99CD9AB0B70B32ECDA51EAAE471625608EA2\",\n" + + " \"issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"value\" : \"100\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmDepositFlags.LP_TOKEN + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMDeposit\"\n" + + "}"; + + assertCanSerializeAndDeserialize(deposit, json); + } + + @Test + void constructTwoAssetDepositAndTestJson() throws JSONException, JsonProcessingException { + AmmDeposit deposit = AmmDeposit.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .flags(AmmDepositFlags.TWO_ASSET) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .amount( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ) + .amount2(XrpCurrencyAmount.ofDrops(10)) + .build(); + + assertThat(deposit.flags()).isEqualTo(AmmDepositFlags.TWO_ASSET); + + String json = "{\n" + + " \"Account\" : \"" + deposit.account() + "\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"039C99CD9AB0B70B32ECDA51EAAE471625608EA2\",\n" + + " \"issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"value\" : \"100\"\n" + + " },\n" + + " \"Amount2\" : \"10\"," + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmDepositFlags.TWO_ASSET + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMDeposit\"\n" + + "}"; + + assertCanSerializeAndDeserialize(deposit, json); + } + + @Test + void constructSingleAssetDepositAndTestJson() throws JSONException, JsonProcessingException { + AmmDeposit deposit = AmmDeposit.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .flags(AmmDepositFlags.SINGLE_ASSET) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .amount( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .build(); + + assertThat(deposit.flags()).isEqualTo(AmmDepositFlags.SINGLE_ASSET); + + String json = "{\n" + + " \"Account\" : \"" + deposit.account() + "\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"039C99CD9AB0B70B32ECDA51EAAE471625608EA2\",\n" + + " \"issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"value\" : \"100\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmDepositFlags.SINGLE_ASSET + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMDeposit\"\n" + + "}"; + + assertCanSerializeAndDeserialize(deposit, json); + } + + @Test + void constructOneAssetLpTokenDepositAndTestJson() throws JSONException, JsonProcessingException { + AmmDeposit deposit = AmmDeposit.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .flags(AmmDepositFlags.ONE_ASSET_LP_TOKEN) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .amount( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ) + .lpTokenOut( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ) + .build(); + + assertThat(deposit.flags()).isEqualTo(AmmDepositFlags.ONE_ASSET_LP_TOKEN); + + String json = "{\n" + + " \"Account\" : \"" + deposit.account() + "\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"039C99CD9AB0B70B32ECDA51EAAE471625608EA2\",\n" + + " \"issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"value\" : \"100\"\n" + + " },\n" + + " \"LPTokenOut\" : {\n" + + " \"currency\" : \"039C99CD9AB0B70B32ECDA51EAAE471625608EA2\",\n" + + " \"issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"value\" : \"100\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmDepositFlags.ONE_ASSET_LP_TOKEN + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMDeposit\"\n" + + "}"; + + assertCanSerializeAndDeserialize(deposit, json); + } + + @Test + void constructLimitLpTokenDepositAndTestJson() throws JSONException, JsonProcessingException { + AmmDeposit deposit = AmmDeposit.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .flags(AmmDepositFlags.LIMIT_LP_TOKEN) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .amount( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ) + .effectivePrice(XrpCurrencyAmount.ofDrops(10)) + .build(); + + assertThat(deposit.flags()).isEqualTo(AmmDepositFlags.LIMIT_LP_TOKEN); + + String json = "{\n" + + " \"Account\" : \"" + deposit.account() + "\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"039C99CD9AB0B70B32ECDA51EAAE471625608EA2\",\n" + + " \"issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"value\" : \"100\"\n" + + " },\n" + + " \"EPrice\" : \"10\",\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmDepositFlags.LIMIT_LP_TOKEN + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMDeposit\"\n" + + "}"; + + assertCanSerializeAndDeserialize(deposit, json); + } + + @Test + void constructTwoAssetIfEmptyDepositTestJson() throws JSONException, JsonProcessingException { + AmmDeposit deposit = AmmDeposit.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .flags(AmmDepositFlags.TWO_ASSET_IF_EMPTY) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .amount( + IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build() + ) + .amount2(XrpCurrencyAmount.ofDrops(10)) + .effectivePrice(XrpCurrencyAmount.ofDrops(10)) + .build(); + + assertThat(deposit.flags()).isEqualTo(AmmDepositFlags.TWO_ASSET_IF_EMPTY); + + String json = "{\n" + + " \"Account\" : \"" + deposit.account() + "\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"039C99CD9AB0B70B32ECDA51EAAE471625608EA2\",\n" + + " \"issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" + + " \"value\" : \"100\"\n" + + " },\n" + + " \"Amount2\" : \"10\",\n" + + " \"EPrice\" : \"10\",\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmDepositFlags.TWO_ASSET_IF_EMPTY + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMDeposit\"\n" + + "}"; + + assertCanSerializeAndDeserialize(deposit, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmVoteTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmVoteTest.java new file mode 100644 index 000000000..e0fee4500 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmVoteTest.java @@ -0,0 +1,131 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; +import org.xrpl.xrpl4j.model.ledger.Issue; + +class AmmVoteTest extends AbstractJsonTest { + + @Test + void testJson() throws JSONException, JsonProcessingException { + AmmVote vote = AmmVote.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .build() + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(8)) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(600))) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Sequence\" : 8,\n" + + " \"TradingFee\" : 600,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMVote\"\n" + + "}"; + + assertCanSerializeAndDeserialize(vote, json); + } + + @Test + void testJsonWithUnsetFlags() throws JSONException, JsonProcessingException { + AmmVote vote = AmmVote.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .build() + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(8)) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(600))) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .flags(TransactionFlags.UNSET) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : 0,\n" + + " \"Sequence\" : 8,\n" + + " \"TradingFee\" : 600,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMVote\"\n" + + "}"; + + assertCanSerializeAndDeserialize(vote, json); + } + + @Test + void testJsonWithNonZeroFlags() throws JSONException, JsonProcessingException { + AmmVote vote = AmmVote.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .build() + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(8)) + .tradingFee(TradingFee.of(UnsignedInteger.valueOf(600))) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .flags(TransactionFlags.FULLY_CANONICAL_SIG) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : 2147483648,\n" + + " \"Sequence\" : 8,\n" + + " \"TradingFee\" : 600,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMVote\"\n" + + "}"; + + assertCanSerializeAndDeserialize(vote, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmWithdrawTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmWithdrawTest.java new file mode 100644 index 000000000..493af7fe3 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmWithdrawTest.java @@ -0,0 +1,223 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.AmmWithdrawFlags; +import org.xrpl.xrpl4j.model.ledger.Issue; + +class AmmWithdrawTest extends AbstractJsonTest { + + @Test + void constructLpTokenWithdrawAndTestJson() throws JSONException, JsonProcessingException { + AmmWithdraw withdraw = baseBuilder() + .flags(AmmWithdrawFlags.LP_TOKEN) + .lpTokensIn(lpTokensIn()) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"LPTokensIn\" : " + objectMapper.writeValueAsString(withdraw.lpTokensIn()) + "," + + " \"Asset\" : " + objectMapper.writeValueAsString(withdraw.asset()) + "," + + " \"Asset2\" : " + objectMapper.writeValueAsString(withdraw.asset2()) + "," + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmWithdrawFlags.LP_TOKEN + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMWithdraw\"\n" + + "}"; + + assertCanSerializeAndDeserialize(withdraw, json); + } + + @Test + void constructWithdrawAllAndTestJson() throws JSONException, JsonProcessingException { + AmmWithdraw withdraw = baseBuilder() + .flags(AmmWithdrawFlags.WITHDRAW_ALL) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Asset\" : " + objectMapper.writeValueAsString(withdraw.asset()) + "," + + " \"Asset2\" : " + objectMapper.writeValueAsString(withdraw.asset2()) + "," + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmWithdrawFlags.WITHDRAW_ALL + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMWithdraw\"\n" + + "}"; + + assertCanSerializeAndDeserialize(withdraw, json); + } + + @Test + void constructTwoAssetAndTestJson() throws JSONException, JsonProcessingException { + AmmWithdraw withdraw = baseBuilder() + .flags(AmmWithdrawFlags.TWO_ASSET) + .amount(amount()) + .amount2(XrpCurrencyAmount.ofDrops(50000000)) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\",\n" + + " \"value\" : \"5\"\n" + + " },\n" + + " \"Amount2\" : \"50000000\"," + + " \"Asset\" : " + objectMapper.writeValueAsString(withdraw.asset()) + "," + + " \"Asset2\" : " + objectMapper.writeValueAsString(withdraw.asset2()) + "," + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmWithdrawFlags.TWO_ASSET + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMWithdraw\"\n" + + "}"; + + assertCanSerializeAndDeserialize(withdraw, json); + } + + @Test + void constructSingleAssetAndTestJson() throws JSONException, JsonProcessingException { + AmmWithdraw withdraw = baseBuilder() + .flags(AmmWithdrawFlags.SINGLE_ASSET) + .amount(amount()) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\",\n" + + " \"value\" : \"5\"\n" + + " },\n" + + " \"Asset\" : " + objectMapper.writeValueAsString(withdraw.asset()) + "," + + " \"Asset2\" : " + objectMapper.writeValueAsString(withdraw.asset2()) + "," + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmWithdrawFlags.SINGLE_ASSET + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMWithdraw\"\n" + + "}"; + + assertCanSerializeAndDeserialize(withdraw, json); + } + + @Test + void constructOneAssetWithdrawAllAndTestJson() throws JSONException, JsonProcessingException { + AmmWithdraw withdraw = baseBuilder() + .flags(AmmWithdrawFlags.ONE_ASSET_WITHDRAW_ALL) + .amount(amount()) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\",\n" + + " \"value\" : \"5\"\n" + + " },\n" + + " \"Asset\" : " + objectMapper.writeValueAsString(withdraw.asset()) + "," + + " \"Asset2\" : " + objectMapper.writeValueAsString(withdraw.asset2()) + "," + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmWithdrawFlags.ONE_ASSET_WITHDRAW_ALL + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMWithdraw\"\n" + + "}"; + + assertCanSerializeAndDeserialize(withdraw, json); + } + + @Test + void constructOneAssetLpTokenAndTestJson() throws JSONException, JsonProcessingException { + AmmWithdraw withdraw = baseBuilder() + .flags(AmmWithdrawFlags.ONE_ASSET_LP_TOKEN) + .amount(amount()) + .lpTokensIn(lpTokensIn()) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\",\n" + + " \"value\" : \"5\"\n" + + " },\n" + + " \"LPTokensIn\" : " + objectMapper.writeValueAsString(withdraw.lpTokensIn()) + "," + + " \"Asset\" : " + objectMapper.writeValueAsString(withdraw.asset()) + "," + + " \"Asset2\" : " + objectMapper.writeValueAsString(withdraw.asset2()) + "," + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmWithdrawFlags.ONE_ASSET_LP_TOKEN + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMWithdraw\"\n" + + "}"; + + assertCanSerializeAndDeserialize(withdraw, json); + } + + @Test + void constructLimitLpTokenAndTestJson() throws JSONException, JsonProcessingException { + AmmWithdraw withdraw = baseBuilder() + .flags(AmmWithdrawFlags.LIMIT_LP_TOKEN) + .amount(amount()) + .effectivePrice(amount()) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"Amount\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\",\n" + + " \"value\" : \"5\"\n" + + " },\n" + + " \"EPrice\" : " + objectMapper.writeValueAsString(withdraw.effectivePrice()) + "," + + " \"Asset\" : " + objectMapper.writeValueAsString(withdraw.asset()) + "," + + " \"Asset2\" : " + objectMapper.writeValueAsString(withdraw.asset2()) + "," + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmWithdrawFlags.LIMIT_LP_TOKEN + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMWithdraw\"\n" + + "}"; + + assertCanSerializeAndDeserialize(withdraw, json); + } + + private ImmutableIssuedCurrencyAmount amount() { + return IssuedCurrencyAmount.builder() + .currency("TST") + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .value("5") + .build(); + } + + private ImmutableIssuedCurrencyAmount lpTokensIn() { + return IssuedCurrencyAmount.builder() + .currency("039C99CD9AB0B70B32ECDA51EAAE471625608EA2") + .issuer(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S")) + .value("100") + .build(); + } + + private ImmutableAmmWithdraw.Builder baseBuilder() { + return AmmWithdraw.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .fee(XrpCurrencyAmount.ofDrops(10)) + .asset( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ).asset2(Issue.XRP); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/ClawbackTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/ClawbackTest.java new file mode 100644 index 000000000..5eeb0410a --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/ClawbackTest.java @@ -0,0 +1,118 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; + +class ClawbackTest extends AbstractJsonTest { + + @Test + void testJsonWithoutFlags() throws JSONException, JsonProcessingException { + Clawback clawback = Clawback.builder() + .account(Address.of("rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.ONE) + .signingPublicKey(PublicKey.fromBase16EncodedPublicKey( + "02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC" + )) + .amount( + IssuedCurrencyAmount.builder() + .currency("FOO") + .issuer(Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW")) + .value("314.159") + .build() + ) + .build(); + + String json = "{\n" + + " \"TransactionType\": \"Clawback\",\n" + + " \"Account\": \"rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S\",\n" + + " \"Amount\": {\n" + + " \"currency\": \"FOO\",\n" + + " \"issuer\": \"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW\",\n" + + " \"value\": \"314.159\"\n" + + " },\n" + + " \"Fee\": \"10\",\n" + + " \"Sequence\": 1,\n" + + " \"SigningPubKey\": \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\"\n" + + "}"; + + assertCanSerializeAndDeserialize(clawback, json); + } + + @Test + void testJsonWithZeroFlags() throws JSONException, JsonProcessingException { + Clawback clawback = Clawback.builder() + .account(Address.of("rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.ONE) + .signingPublicKey(PublicKey.fromBase16EncodedPublicKey( + "02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC" + )) + .flags(TransactionFlags.UNSET) + .amount( + IssuedCurrencyAmount.builder() + .currency("FOO") + .issuer(Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW")) + .value("314.159") + .build() + ) + .build(); + + String json = "{\n" + + " \"TransactionType\": \"Clawback\",\n" + + " \"Account\": \"rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S\",\n" + + " \"Amount\": {\n" + + " \"currency\": \"FOO\",\n" + + " \"issuer\": \"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW\",\n" + + " \"value\": \"314.159\"\n" + + " },\n" + + " \"Fee\": \"10\",\n" + + " \"Flags\": 0,\n" + + " \"Sequence\": 1,\n" + + " \"SigningPubKey\": \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\"\n" + + "}"; + + assertCanSerializeAndDeserialize(clawback, json); + } + + @Test + void testJsonWithNonZeroFlags() throws JSONException, JsonProcessingException { + Clawback clawback = Clawback.builder() + .account(Address.of("rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.ONE) + .signingPublicKey(PublicKey.fromBase16EncodedPublicKey( + "02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC" + )) + .flags(TransactionFlags.FULLY_CANONICAL_SIG) + .amount( + IssuedCurrencyAmount.builder() + .currency("FOO") + .issuer(Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW")) + .value("314.159") + .build() + ) + .build(); + + String json = String.format("{\n" + + " \"TransactionType\": \"Clawback\",\n" + + " \"Account\": \"rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S\",\n" + + " \"Amount\": {\n" + + " \"currency\": \"FOO\",\n" + + " \"issuer\": \"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW\",\n" + + " \"value\": \"314.159\"\n" + + " },\n" + + " \"Fee\": \"10\",\n" + + " \"Flags\": %s,\n" + + " \"Sequence\": 1,\n" + + " \"SigningPubKey\": \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\"\n" + + "}", TransactionFlags.FULLY_CANONICAL_SIG); + + assertCanSerializeAndDeserialize(clawback, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/CurrencyAmountTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/CurrencyAmountTest.java index f7129de5a..87c494d2b 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/CurrencyAmountTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/CurrencyAmountTest.java @@ -24,8 +24,15 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.io.BaseEncoding; import com.google.common.primitives.UnsignedLong; import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.codec.addresses.ByteUtils; +import org.xrpl.xrpl4j.codec.binary.XrplBinaryCodec; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; + +import java.nio.charset.StandardCharsets; /** * Unit tests for {@link CurrencyAmount}. @@ -134,6 +141,80 @@ public void mapIssuance() { ); } + /** + * Verify that xrpl4j is unaffected by the bug reported in rippled issue #4112. When rippled APIs are provided + * 3-character currency codes, those APIs will upper-case the supplied currency values. Only after that normalization + * will those APIs then convert to binary. For example, if a request is made to rippled to sign a payload with a + * currency code of `UsD`, the API layer will normalize this value to `USD` (i.e., all-caps) before signing. However, + * tooling (like xrpl4j) is not forced to do this kind of upper-case normalization. So, it's possible for any tooling + * (like xrpl4j) to unintentionally allow issuers to issue mixed-case, 3-character currency codes. However, there's + * debate in the GH issue linked below about whether this is a bug in tooling (like xrpl4j), or if this is actually a + * bug in the rippled code base (in which case, the normalization functionality should be removed from rippled, and + * tooling should do nothing). Contributors to the issue assert the latter -- i.e., it's a bug in rippled and should + * be removed from rippled. There is also PR to this effect. Thus, this test ensures that xrpl4j tooling does the + * correct thing (i.e., no currency code normalization, either in our Transaction layer or in the binary codec). + * + * @see "https://github.com/XRPLF/rippled/issues/4112" + */ + @Test + public void buildIssuanceWithMixedCaseThreeCharacterCode() { + final IssuedCurrencyAmount issuedCurrencyAmount = IssuedCurrencyAmount.builder() + .issuer(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .currency("UsD") + .value("100") + .build(); + + assertThat(issuedCurrencyAmount.currency()).isEqualTo("UsD"); + } + + @Test + public void encodeDecodeMixedCaseCurrencyCode() throws JsonProcessingException { + currencyTestHelper("Usd"); + currencyTestHelper("UsD"); + currencyTestHelper("USD"); + currencyTestHelper("$GHOST"); + currencyTestHelper("$ghost"); + currencyTestHelper("$ghosT"); + } + + /** + * Helper method to test various currencies codes for capitalization. + * + * @param currencyCode A {@link String} representing a currency code. + */ + private void currencyTestHelper(String currencyCode) throws JsonProcessingException { + if (currencyCode.length() > 3) { + currencyCode = ByteUtils.padded( + BaseEncoding.base16().encode(currencyCode.getBytes(StandardCharsets.US_ASCII)), + 40 // <-- Non-standard currency codes must be 40 bytes. + ); + } + + final CurrencyAmount issuedCurrencyAmountMixed = IssuedCurrencyAmount.builder() + .issuer(Address.of("rPx8CtHbTkjYbQzrwfDxXfPfLHV9nbjYBz")) + .currency(currencyCode) + .value("100") + .build(); + + Payment payment = Payment.builder() + .account(Address.of("rPx8CtHbTkjYbQzrwfDxXfPfLHV9nbjYBz")) + .destination(Address.of("rPx8CtHbTkjYbQzrwfDxXfPfLHV9nbjYBz")) + .amount(issuedCurrencyAmountMixed) + .fee(XrpCurrencyAmount.of(UnsignedLong.ONE)) + .build(); + + String transactionJson = ObjectMapperFactory.create().writeValueAsString(payment); + String transactionBinary = XrplBinaryCodec.getInstance().encode(transactionJson); + String decodedTransactionJson = XrplBinaryCodec.getInstance().decode(transactionBinary); + Payment decodedPayment = ObjectMapperFactory.create().readValue(decodedTransactionJson, Payment.class); + + final String finalCurrencyCode = currencyCode; + decodedPayment.amount().handle( + xrpCurrencyAmount -> fail(), + issuedCurrencyAmount -> assertThat(issuedCurrencyAmount.currency()).isEqualTo(finalCurrencyCode) + ); + } + @Test void testConstants() { assertThat(CurrencyAmount.ONE_XRP_IN_DROPS).isEqualTo(1_000_000L); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/EscrowFinishTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/EscrowFinishTest.java index 804f8005f..232bf6b6b 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/EscrowFinishTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/EscrowFinishTest.java @@ -21,13 +21,21 @@ */ import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.io.BaseEncoding; import com.google.common.primitives.UnsignedInteger; +import com.ripple.cryptoconditions.Condition; +import com.ripple.cryptoconditions.CryptoConditionReader; import com.ripple.cryptoconditions.Fulfillment; +import com.ripple.cryptoconditions.PreimageSha256Condition; import com.ripple.cryptoconditions.PreimageSha256Fulfillment; +import com.ripple.cryptoconditions.der.DerEncodingException; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; import org.xrpl.xrpl4j.model.transactions.ImmutableEscrowFinish.Builder; /** @@ -35,8 +43,27 @@ */ public class EscrowFinishTest { + public static final String GOOD_CONDITION_STR = + "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100"; + + public static final String GOOD_FULFILLMENT_STR = + "A0028000"; + + private static Condition condition; + private static Fulfillment fulfillment; + + @BeforeAll + static void beforeAll() { + try { + condition = CryptoConditionReader.readCondition(BaseEncoding.base16().decode(GOOD_CONDITION_STR)); + fulfillment = CryptoConditionReader.readFulfillment(BaseEncoding.base16().decode(GOOD_FULFILLMENT_STR)); + } catch (DerEncodingException e) { + throw new RuntimeException(e); + } + } + @Test - public void testNormalizeWithNoFulfillmentNoCondition() { + public void constructWithNoFulfillmentNoCondition() { EscrowFinish actual = EscrowFinish.builder() .fee(XrpCurrencyAmount.ofDrops(1)) .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) @@ -46,7 +73,9 @@ public void testNormalizeWithNoFulfillmentNoCondition() { .build(); assertThat(actual.condition()).isNotPresent(); + assertThat(actual.conditionRawValue()).isNotPresent(); assertThat(actual.fulfillment()).isNotPresent(); + assertThat(actual.fulfillmentRawValue()).isNotPresent(); assertThat(actual.fee()).isEqualTo(XrpCurrencyAmount.ofDrops(1)); assertThat(actual.account()).isEqualTo(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")); assertThat(actual.sequence()).isEqualTo(UnsignedInteger.ONE); @@ -54,86 +83,220 @@ public void testNormalizeWithNoFulfillmentNoCondition() { assertThat(actual.offerSequence()).isEqualTo(UnsignedInteger.ZERO); } + //////////////////////////////// + // normalizeCondition tests + //////////////////////////////// + + @Test + void normalizeWithNoConditionNoRawValue() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .build(); + + assertThat(actual.condition()).isEmpty(); + assertThat(actual.conditionRawValue()).isEmpty(); + } + + @Test + void normalizeWithConditionAndRawValueMatching() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .condition(condition) + .conditionRawValue(GOOD_CONDITION_STR) + .build(); + + assertThat(actual.condition()).isNotEmpty().get().isEqualTo(condition); + assertThat(actual.conditionRawValue()).isNotEmpty().get().isEqualTo(GOOD_CONDITION_STR); + } + + @Test + void normalizeWithConditionAndRawValueNonMatching() { + assertThatThrownBy(() -> EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .condition(condition) + // This is slightly different than GOOD_CONDITION_STR + .conditionRawValue("A0258020E3B0C44298FC1C149ABCD4C8996FB92427AE41E4649B934CA495991B7852B855810100") + .build() + ).isInstanceOf(IllegalStateException.class) + .hasMessage("condition and conditionRawValue should be equivalent if both are present."); + } + @Test - public void testNormalizeWithFulfillmentNoCondition() { - Fulfillment fulfillment = PreimageSha256Fulfillment.from("ssh".getBytes()); - - assertThrows( - IllegalStateException.class, - () -> EscrowFinish.builder() - .fee(XrpCurrencyAmount.ofDrops(1)) - .account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .sequence(UnsignedInteger.ONE) - .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .offerSequence(UnsignedInteger.ZERO) - .fulfillment(fulfillment) - .build(), - "If a fulfillment is specified, the corresponding condition must also be specified." - ); + void normalizeWithConditionPresentAndNoRawValue() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .condition(condition) + .build(); + + assertThat(actual.conditionRawValue()).isNotEmpty().get().isEqualTo(GOOD_CONDITION_STR); } @Test - public void testNormalizeWithNoFulfillmentAndCondition() { - Fulfillment fulfillment = PreimageSha256Fulfillment.from("ssh".getBytes()); - - assertThrows( - IllegalStateException.class, - () -> EscrowFinish.builder() - .fee(XrpCurrencyAmount.ofDrops(1)) - .account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .sequence(UnsignedInteger.ONE) - .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .offerSequence(UnsignedInteger.ZERO) - .condition(fulfillment.getDerivedCondition()) - .build(), - "If a condition is specified, the corresponding fulfillment must also be specified." - ); + void normalizeWithNoConditionAndRawValueForValidCondition() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .conditionRawValue(GOOD_CONDITION_STR) + .build(); + + assertThat(actual.condition()).isNotEmpty().get().isEqualTo(condition); } @Test - public void testNormalizeWithFulfillmentAndConditionButFeeLow() { - // We expect the + void normalizeWithNoConditionAndRawValueForMalformedCondition() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .conditionRawValue("1234") + .build(); - Fulfillment fulfillment = PreimageSha256Fulfillment.from("ssh".getBytes()); + assertThat(actual.condition()).isEmpty(); + assertThat(actual.conditionRawValue()).isNotEmpty().get().isEqualTo("1234"); + } + @Test + void normalizeWithNoConditionAndRawValueForBadHexLengthCondition() { EscrowFinish actual = EscrowFinish.builder() - .fee(XrpCurrencyAmount.ofDrops(330)) - .account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .conditionRawValue("123") + .build(); + + assertThat(actual.condition()).isEmpty(); + assertThat(actual.conditionRawValue()).isNotEmpty().get().isEqualTo("123"); + } + + //////////////////////////////// + // normalizeFulfillment tests + //////////////////////////////// + + @Test + void normalizeWithNoFulfillmentNoRawValue() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .build(); + + assertThat(actual.fulfillment()).isEmpty(); + assertThat(actual.fulfillmentRawValue()).isEmpty(); + } + + @Test + void normalizeWithFulfillmentAndRawValueMatching() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) .sequence(UnsignedInteger.ONE) .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) .offerSequence(UnsignedInteger.ZERO) .fulfillment(fulfillment) - .condition(fulfillment.getDerivedCondition()) + .fulfillmentRawValue(GOOD_FULFILLMENT_STR) .build(); - assertThat(actual.condition()).isPresent(); - assertThat(actual.account()).isEqualTo(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")); - assertThat(actual.sequence()).isEqualTo(UnsignedInteger.ONE); - assertThat(actual.owner()).isEqualTo(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")); - assertThat(actual.offerSequence()).isEqualTo(UnsignedInteger.ZERO); + assertThat(actual.fulfillment()).isNotEmpty().get().isEqualTo(fulfillment); + assertThat(actual.fulfillmentRawValue()).isNotEmpty().get().isEqualTo(GOOD_FULFILLMENT_STR); + } - assertThat(actual.fulfillment()).isPresent(); - assertThat(actual.fulfillment().get()).isEqualTo(fulfillment); - assertThat(actual.fee()).isEqualTo(XrpCurrencyAmount.ofDrops(330)); + @Test + void normalizeWithFulfillmentAndRawValueNonMatching() { + assertThatThrownBy(() -> EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .fulfillment(fulfillment) + // This is slightly different than GOOD_FULFILLMENT_STR + .fulfillmentRawValue("A0011000") + .build() + ).isInstanceOf(IllegalStateException.class) + .hasMessage("fulfillment and fulfillmentRawValue should be equivalent if both are present."); } @Test - public void testNormalizeWithFeeTooLow() { - Fulfillment fulfillment = PreimageSha256Fulfillment.from("ssh".getBytes()); - - assertThrows( - IllegalStateException.class, - () -> EscrowFinish.builder() - .fee(XrpCurrencyAmount.ofDrops(1)) - .account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .sequence(UnsignedInteger.ONE) - .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .offerSequence(UnsignedInteger.ZERO) - .fulfillment(fulfillment) - .condition(fulfillment.getDerivedCondition()) - .build(), - "If a fulfillment is specified, the fee must be set to 330 or greater." - ); + void normalizeWithFulfillmentPresentAndNoRawValue() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .fulfillment(fulfillment) + .build(); + + assertThat(actual.fulfillmentRawValue()).isNotEmpty().get().isEqualTo(GOOD_FULFILLMENT_STR); + } + + @Test + void normalizeWithNoFulfillmentAndRawValueForValidFulfillment() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .fulfillmentRawValue(GOOD_FULFILLMENT_STR) + .build(); + + assertThat(actual.fulfillment()).isNotEmpty().get().isEqualTo(fulfillment); + } + + @Test + void normalizeWithNoFulfillmentAndRawValueForMalformedFulfillment() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .fulfillmentRawValue("1234") + .build(); + + assertThat(actual.fulfillment()).isEmpty(); + assertThat(actual.fulfillmentRawValue()).isNotEmpty().get().isEqualTo("1234"); + } + + @Test + void normalizeWithNoFulfillmentAndRawValueForBadHexLengthFulfillment() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .fulfillmentRawValue("123") + .build(); + + assertThat(actual.fulfillment()).isEmpty(); + assertThat(actual.fulfillmentRawValue()).isNotEmpty().get().isEqualTo("123"); } @Test diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TradingFeeTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TradingFeeTest.java new file mode 100644 index 000000000..16aa2f272 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TradingFeeTest.java @@ -0,0 +1,105 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.primitives.UnsignedInteger; +import org.assertj.core.api.Assertions; +import org.immutables.value.Value; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; + +import java.math.BigDecimal; + +public class TradingFeeTest { + + ObjectMapper objectMapper = ObjectMapperFactory.create(); + + @Test + void fromUnsignedInteger() { + TradingFee fee = TradingFee.of(UnsignedInteger.ONE); + assertThat(fee.toString()).isEqualTo("1"); + assertThat(fee.value()).isEqualTo(UnsignedInteger.ONE); + } + + @Test + void ofPercent() { + TradingFee fee = TradingFee.ofPercent(BigDecimal.valueOf(0.99900)); + assertThat(fee.value()).isEqualTo(UnsignedInteger.valueOf(999)); + + fee = TradingFee.ofPercent(BigDecimal.valueOf(1.0000)); + assertThat(fee.value()).isEqualTo(UnsignedInteger.valueOf(1000)); + + fee = TradingFee.ofPercent(BigDecimal.valueOf(0)); + assertThat(fee.value()).isEqualTo(UnsignedInteger.valueOf(0)); + + fee = TradingFee.ofPercent(BigDecimal.valueOf(0.00000000000)); + assertThat(fee.value()).isEqualTo(UnsignedInteger.valueOf(0)); + } + + @Test + void ofPercentWithTooManyDecimalPlaces() { + assertThatThrownBy( + () -> TradingFee.ofPercent(BigDecimal.valueOf(0.0001)) + ).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Percent value should have a maximum of 3 decimal places."); + + assertThatThrownBy( + () -> TradingFee.ofPercent(BigDecimal.valueOf(0.0001000)) + ).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Percent value should have a maximum of 3 decimal places."); + } + + @Test + void bigDecimalValue() { + BigDecimal percent = BigDecimal.valueOf(0.001000); + TradingFee fee = TradingFee.ofPercent(percent); + assertThat(fee.bigDecimalValue()).isEqualTo(percent); + + percent = BigDecimal.valueOf(1, 3); + fee = TradingFee.ofPercent(percent); + assertThat(fee.bigDecimalValue()).isEqualTo(percent); + } + + @Test + void testJson() throws JsonProcessingException, JSONException { + TradingFee tradingFee = TradingFee.of(UnsignedInteger.valueOf(1000)); + TradingFeeWrapper wrapper = TradingFeeWrapper.of(tradingFee); + + String json = "{\"tradingFee\": 1000}"; + assertSerializesAndDeserializes(wrapper, json); + } + + private void assertSerializesAndDeserializes( + TradingFeeWrapper wrapper, + String json + ) throws JsonProcessingException, JSONException { + String serialized = objectMapper.writeValueAsString(wrapper); + JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); + TradingFeeWrapper deserialized = objectMapper.readValue( + serialized, TradingFeeWrapper.class + ); + Assertions.assertThat(deserialized).isEqualTo(wrapper); + } + + @Value.Immutable + @JsonSerialize(as = ImmutableTradingFeeWrapper.class) + @JsonDeserialize(as = ImmutableTradingFeeWrapper.class) + interface TradingFeeWrapper { + + static TradingFeeWrapper of(TradingFee tradingFee) { + return ImmutableTradingFeeWrapper.builder().tradingFee(tradingFee).build(); + } + + TradingFee tradingFee(); + + } +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TransactionMetadataTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TransactionMetadataTest.java index d572a6f42..1719a471a 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TransactionMetadataTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/TransactionMetadataTest.java @@ -21,6 +21,7 @@ import org.xrpl.xrpl4j.model.transactions.metadata.ImmutableDeletedNode; import org.xrpl.xrpl4j.model.transactions.metadata.ImmutableMetaNfTokenOfferObject; import org.xrpl.xrpl4j.model.transactions.metadata.MetaAccountRootObject; +import org.xrpl.xrpl4j.model.transactions.metadata.MetaAmmObject; import org.xrpl.xrpl4j.model.transactions.metadata.MetaCheckObject; import org.xrpl.xrpl4j.model.transactions.metadata.MetaDepositPreAuthObject; import org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject; @@ -804,6 +805,8 @@ private Class determineLedgerObjectType(String ledge return MetaTicketObject.class; case "NFTokenPage": return MetaNfTokenPageObject.class; + case "AMM": + return MetaAmmObject.class; default: return MetaUnknownObject.class; } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/VoteWeightTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/VoteWeightTest.java new file mode 100644 index 000000000..1c7180276 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/VoteWeightTest.java @@ -0,0 +1,81 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.primitives.UnsignedInteger; +import org.assertj.core.api.Assertions; +import org.immutables.value.Value; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; + +import java.math.BigDecimal; + +public class VoteWeightTest { + + ObjectMapper objectMapper = ObjectMapperFactory.create(); + + @Test + void bigDecimalValue() { + VoteWeight weight = VoteWeight.of(UnsignedInteger.ZERO); + assertThat(weight.bigDecimalValue().compareTo(BigDecimal.ZERO)).isEqualTo(0); + + weight = VoteWeight.of(UnsignedInteger.ONE); + assertThat(weight.bigDecimalValue().compareTo(BigDecimal.valueOf(0.001))).isEqualTo(0); + + weight = VoteWeight.of(UnsignedInteger.valueOf(999)); + assertThat(weight.bigDecimalValue().compareTo(BigDecimal.valueOf(0.999))).isEqualTo(0); + + weight = VoteWeight.of(UnsignedInteger.valueOf(99_000)); + assertThat(weight.bigDecimalValue().compareTo(BigDecimal.valueOf(99.000))).isEqualTo(0); + + weight = VoteWeight.of(UnsignedInteger.valueOf(100_000)); + assertThat(weight.bigDecimalValue().compareTo(BigDecimal.valueOf(100.000))).isEqualTo(0); + } + + @Test + void testToString() { + VoteWeight weight = VoteWeight.of(UnsignedInteger.ZERO); + assertThat(weight.toString()).isEqualTo(UnsignedInteger.ZERO.toString()); + } + + @Test + void testJson() throws JsonProcessingException, JSONException { + VoteWeight voteWeight = VoteWeight.of(UnsignedInteger.valueOf(1000)); + VoteWeightWrapper wrapper = VoteWeightWrapper.of(voteWeight); + + String json = "{\"voteWeight\": 1000}"; + assertSerializesAndDeserializes(wrapper, json); + } + + private void assertSerializesAndDeserializes( + VoteWeightWrapper wrapper, + String json + ) throws JsonProcessingException, JSONException { + String serialized = objectMapper.writeValueAsString(wrapper); + JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); + VoteWeightWrapper deserialized = objectMapper.readValue( + serialized, VoteWeightWrapper.class + ); + Assertions.assertThat(deserialized).isEqualTo(wrapper); + } + + @Value.Immutable + @JsonSerialize(as = ImmutableVoteWeightWrapper.class) + @JsonDeserialize(as = ImmutableVoteWeightWrapper.class) + interface VoteWeightWrapper { + + static VoteWeightWrapper of(VoteWeight voteWeight) { + return ImmutableVoteWeightWrapper.builder().voteWeight(voteWeight).build(); + } + + VoteWeight voteWeight(); + + } +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/AccountSetJsonTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/AccountSetJsonTests.java index 764e91430..6f6447209 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/AccountSetJsonTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/AccountSetJsonTests.java @@ -25,6 +25,7 @@ import org.json.JSONException; import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.crypto.signing.Signature; import org.xrpl.xrpl4j.model.AbstractJsonTest; import org.xrpl.xrpl4j.model.flags.AccountSetTransactionFlags; import org.xrpl.xrpl4j.model.flags.TransactionFlags; @@ -194,4 +195,34 @@ void testJsonWithZeroClearFlagAndSetFlag() throws JSONException, JsonProcessingE assertCanSerializeAndDeserialize(accountSet, json); } + + @Test + void testJsonWithUnrecognizedClearAndSetFlag() throws JSONException, JsonProcessingException { + AccountSet accountSet = AccountSet.builder() + .account(Address.of("rhyg7sn3ZQj9aja9CBuLETFKdkG9Fte7ck")) + .fee(XrpCurrencyAmount.ofDrops(15)) + .sequence(UnsignedInteger.valueOf(40232131)) + .setFlagRawValue(UnsignedInteger.valueOf(4294967295L)) + .clearFlagRawValue(UnsignedInteger.valueOf(4294967295L)) + .signingPublicKey(PublicKey.fromBase16EncodedPublicKey( + "ED03FCED79BB3699089BC3F0F57BBD9F9ABA4772C20EC14BFAE908B4299344194A" + )) + .transactionSignature(Signature.fromBase16("8D0915449FB617234DD54E1BA79136B50C4439696BB4287F" + + "CA25717F3FD4875D5A1FEE6269B91D71D9306B48DECAAE1F1C91CAD1AAD0E0D683DC11E68212740E")) + .build(); + + String json = "{\n" + + " \"Account\": \"rhyg7sn3ZQj9aja9CBuLETFKdkG9Fte7ck\",\n" + + " \"Fee\": \"15\",\n" + + " \"Sequence\": 40232131,\n" + + " \"SetFlag\": 4294967295,\n" + + " \"ClearFlag\": 4294967295,\n" + + " \"SigningPubKey\": \"ED03FCED79BB3699089BC3F0F57BBD9F9ABA4772C20EC14BFAE908B4299344194A\",\n" + + " \"TransactionType\": \"AccountSet\",\n" + + " \"TxnSignature\": \"8D0915449FB617234DD54E1BA79136B50C4439696BB4287FCA25717F3FD4875D5A1FEE6269B" + + "91D71D9306B48DECAAE1F1C91CAD1AAD0E0D683DC11E68212740E\"" + + "}"; + + assertCanSerializeAndDeserialize(accountSet, json); + } } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/EscrowJsonTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/EscrowJsonTests.java index df7609290..6c7c39f87 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/EscrowJsonTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/EscrowJsonTests.java @@ -20,14 +20,13 @@ * =========================LICENSE_END================================== */ -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.io.BaseEncoding; import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; import com.ripple.cryptoconditions.CryptoConditionReader; -import com.ripple.cryptoconditions.PreimageSha256Fulfillment; import com.ripple.cryptoconditions.der.DerEncodingException; import org.json.JSONException; import org.junit.jupiter.api.Test; @@ -352,19 +351,46 @@ public void testEscrowFinishJsonWithNonZeroFlags() } @Test - public void testEscrowFinishJsonWithFeeTooLow() { - assertThrows( - IllegalStateException.class, - () -> EscrowFinish.builder() - .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) - .fee(XrpCurrencyAmount.ofDrops(3)) - .sequence(UnsignedInteger.ONE) - .owner(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) - .offerSequence(UnsignedInteger.valueOf(7)) - .condition(PreimageSha256Fulfillment.from(new byte[48]).getDerivedCondition()) - .fulfillment(PreimageSha256Fulfillment.from(new byte[60])) - .build(), - "If a fulfillment is specified, the fee must be set to 330 or greater." - ); + void testEscrowFinishJsonWithMalformedCondition() throws JsonProcessingException { + String json = "{\n" + + " \"Account\": \"rKWZ2fDqE5B9XorAcEQZD46H6HEdJQVNdb\",\n" + + " \"Condition\": \"A02580209423ED2EF4CACA8CA4AFC08D3F5EC60A545FD7A97E66E16EA0E2E2\",\n" + + " \"Fee\": \"563\",\n" + + " \"Fulfillment\": \"A02280203377850F1B3A4322F1562DF6F75D584596ABE5B9C76EEA8301F56CB942ACC69B\",\n" + + " \"LastLedgerSequence\": 40562057,\n" + + " \"OfferSequence\": 40403748,\n" + + " \"Owner\": \"r3iocgQwoGNCYyvvt8xuWv2XYXx7Z2gQqd\",\n" + + " \"Sequence\": 39899485,\n" + + " \"SigningPubKey\": \"ED09285829A011D520A1873A0E2F1014F5D6B66A6DDE6953FC02C8185EAFA6A1B0\",\n" + + " \"TransactionType\": \"EscrowFinish\",\n" + + " \"TxnSignature\": \"A3E64F6B8D1D7C4FBC9663FCD217F86C3529EC2E2F16442DD48D1B66EEE30EAC2CE960A76080F74BC749" + + "8CA7BBFB822BEE9E8F767114D7B54F7403A7CB672501\"\n" + + "}"; + + EscrowFinish escrowFinish = objectMapper.readValue(json, EscrowFinish.class); + assertThat(escrowFinish.condition()).isEmpty(); + assertThat(escrowFinish.conditionRawValue()).isNotEmpty().get() + .isEqualTo("A02580209423ED2EF4CACA8CA4AFC08D3F5EC60A545FD7A97E66E16EA0E2E2"); + } + + @Test + void testEscrowFinishJsonWithMalformedFulfillment() throws JsonProcessingException { + String json = String.format("{\n" + + " \"Account\": \"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\",\n" + + " \"TransactionType\": \"EscrowFinish\",\n" + + " \"Owner\": \"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\",\n" + + " \"OfferSequence\": 7,\n" + + " \"Condition\": \"A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100\",\n" + + " \"Fulfillment\": \"123\",\n" + + " \"Sequence\": 1,\n" + + " \"Flags\": %s,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"Fee\": \"330\"\n" + + "}", TransactionFlags.FULLY_CANONICAL_SIG.getValue()); + + EscrowFinish escrowFinish = objectMapper.readValue(json, EscrowFinish.class); + assertThat(escrowFinish.fulfillment()).isEmpty(); + assertThat(escrowFinish.fulfillmentRawValue()).isNotEmpty().get() + .isEqualTo("123"); } } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryTypeTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryTypeTest.java index fbabdae69..03ce8f461 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryTypeTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryTypeTest.java @@ -29,6 +29,7 @@ void testConstants() { assertThat(MetaLedgerEntryType.SIGNER_LIST.value()).isEqualTo("SignerList"); assertThat(MetaLedgerEntryType.TICKET.value()).isEqualTo("Ticket"); assertThat(MetaLedgerEntryType.NFTOKEN_PAGE.value()).isEqualTo("NFTokenPage"); + assertThat(MetaLedgerEntryType.AMM.value()).isEqualTo("AMM"); } @Test diff --git a/xrpl4j-core/src/test/resources/codec-fixtures.json b/xrpl4j-core/src/test/resources/codec-fixtures.json index 92ef8d945..e05cff35f 100644 --- a/xrpl4j-core/src/test/resources/codec-fixtures.json +++ b/xrpl4j-core/src/test/resources/codec-fixtures.json @@ -33,9 +33,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "059D1E86DE5DCCCF956BF4799675B2425AF9AD44FE4CCA6FEE1C812EEF6423E6", - "Indexes": [ - "908D554AA0D29F660716A3EE65C61DD886B744DDF60DE70E6B16EADB770635DB" - ] + "Indexes": ["908D554AA0D29F660716A3EE65C61DD886B744DDF60DE70E6B16EADB770635DB"] } }, { @@ -329,9 +327,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "17CC40C6872E0C0E658C49B75D0812A70D4161DDA53324DF51FA58D3819C814B", - "Indexes": [ - "571BF14F28C4D97871CDACD344A8CF57E6BA287BF0440B9E0D0683D02751CC7B" - ] + "Indexes": ["571BF14F28C4D97871CDACD344A8CF57E6BA287BF0440B9E0D0683D02751CC7B"] } }, { @@ -380,9 +376,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "1BCA9161A199AD5E907751CBF3FBA49689D517F0E8EE823AE17B737039B41DE1", - "Indexes": [ - "26B894EE68470AD5AEEB55D5EBF936E6397CEE6957B93C56A2E7882CA9082873" - ] + "Indexes": ["26B894EE68470AD5AEEB55D5EBF936E6397CEE6957B93C56A2E7882CA9082873"] } }, { @@ -446,9 +440,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "1F9FF48419CA69FDDCC294CCEEE608F5F8A8BE11E286AD5743ED2D457C5570C4", - "Indexes": [ - "7D4325BE338A40BBCBCC1F351B3272EB3E76305A878E76603DE206A795871619" - ] + "Indexes": ["7D4325BE338A40BBCBCC1F351B3272EB3E76305A878E76603DE206A795871619"] } }, { @@ -614,9 +606,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "289CFC476B5876F28C8A3B3C5B7058EC2BDF668C37B846EA7E5E1A73A4AA0816", - "Indexes": [ - "BC10E40AFB79298004CDE51CB065DBDCABA86EC406E3A1CF02CE5F8A9628A2BD" - ] + "Indexes": ["BC10E40AFB79298004CDE51CB065DBDCABA86EC406E3A1CF02CE5F8A9628A2BD"] } }, { @@ -719,9 +709,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "2C9F00EFA5CCBD43452EF364B12C8DFCEF2B910336E5EFCE3AA412A556991582", - "Indexes": [ - "F721E924498EE68BFF906CD856E8332073DD350BAC9E8977AC3F31860BA1E33A" - ] + "Indexes": ["F721E924498EE68BFF906CD856E8332073DD350BAC9E8977AC3F31860BA1E33A"] } }, { @@ -758,9 +746,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "2FB4904ACFB96228FC002335B1B5A4C5584D9D727BBE82144F0415EB4EA0C727", - "Indexes": [ - "5F22826818CC83448C9DF34939AB4019D3F80C70DEB8BDBDCF0496A36DC68719" - ], + "Indexes": ["5F22826818CC83448C9DF34939AB4019D3F80C70DEB8BDBDCF0496A36DC68719"], "TakerPaysIssuer": "2B6C42A95B3F7EE1971E4A10098E8F1B5F66AA08" } }, @@ -774,9 +760,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "2FB4904ACFB96228FC002335B1B5A4C5584D9D727BBE82145003BAF82D03A000", - "Indexes": [ - "5B7F148A8DDB4EB7386C9E75C4C1ED918DEDE5C52D5BA51B694D7271EF8BDB46" - ], + "Indexes": ["5B7F148A8DDB4EB7386C9E75C4C1ED918DEDE5C52D5BA51B694D7271EF8BDB46"], "TakerPaysIssuer": "2B6C42A95B3F7EE1971E4A10098E8F1B5F66AA08" } }, @@ -967,9 +951,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "3F2BADB38F12C87D111D3970CD1F05FE698DB86F14DC7C5FAEB05BFB6391B00E", - "Indexes": [ - "73E075E64CA5E7CE60FFCD5359C1D730EDFFEE7C4D992760A87DF7EA0A34E40F" - ] + "Indexes": ["73E075E64CA5E7CE60FFCD5359C1D730EDFFEE7C4D992760A87DF7EA0A34E40F"] } }, { @@ -994,9 +976,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "4235CD082112FB621C02D6DA2E4F4ACFAFC91CB0585E034B936C29ABF4A76B01", - "Indexes": [ - "6C4C3F1C6B9D76A6EF50F377E7C3991825694C604DBE0C1DD09362045EE41997" - ] + "Indexes": ["6C4C3F1C6B9D76A6EF50F377E7C3991825694C604DBE0C1DD09362045EE41997"] } }, { @@ -1095,9 +1075,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "48E91FD14597FB089654DADE7B70EB08CAF421EA611D703F3E871F7D4B5AAB5D", - "Indexes": [ - "25DCAC87FBE4C3B66A1AFDE3C3F98E5A16333975C4FD46682F7497F27DFB9766" - ] + "Indexes": ["25DCAC87FBE4C3B66A1AFDE3C3F98E5A16333975C4FD46682F7497F27DFB9766"] } }, { @@ -1159,9 +1137,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "4EFC0442D07AE681F7FDFAA89C75F06F8E28CFF888593440201B0320E8F2C7BD", - "Indexes": [ - "1595E5D5197330F58A479200A2FDD434D7A244BD1FFEC5E5EE8CF064AE77D3F5" - ] + "Indexes": ["1595E5D5197330F58A479200A2FDD434D7A244BD1FFEC5E5EE8CF064AE77D3F5"] } }, { @@ -1375,9 +1351,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "98082E695CAB618590BEEA0647A5F24D2B610A686ECD49310604FC7431FAAB0D", - "Indexes": [ - "9BF3216E42575CA5A3CB4D0F2021EE81D0F7835BA2EDD78E05CAB44B655962BB" - ] + "Indexes": ["9BF3216E42575CA5A3CB4D0F2021EE81D0F7835BA2EDD78E05CAB44B655962BB"] } }, { @@ -1582,9 +1556,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "62AE37A44FE44BDCFC2BA5DD14D74BEC0AC346DA2DC1F04756044364C5BB0000", - "Indexes": [ - "600A398F57CAE44461B4C8C25DE12AC289F87ED125438440B33B97417FE3D82C" - ], + "Indexes": ["600A398F57CAE44461B4C8C25DE12AC289F87ED125438440B33B97417FE3D82C"], "TakerPaysIssuer": "2B6C42A95B3F7EE1971E4A10098E8F1B5F66AA08" } }, @@ -1960,9 +1932,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "72D60CCD3905A3ABE19049B6EE76E8E0F3A2CBAC852625C757176F1B73EF617F", - "Indexes": [ - "AB124EEAB087452070EC70D9DEA1A22C9766FFBBEE1025FD46495CC74148CCA8" - ] + "Indexes": ["AB124EEAB087452070EC70D9DEA1A22C9766FFBBEE1025FD46495CC74148CCA8"] } }, { @@ -2091,9 +2061,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "80AB25842B230D48027800213EB86023A3EAF4430E22C092D333795FFF1E5219", - "Indexes": [ - "42E28285A82D01DCA856118A064C8AEEE1BF8167C08186DA5BFC678687E86F7C" - ] + "Indexes": ["42E28285A82D01DCA856118A064C8AEEE1BF8167C08186DA5BFC678687E86F7C"] } }, { @@ -2218,9 +2186,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "8ADF3C5527CCF6D0B5863365EF40254171536C3901F1CBD9E2BC5F918A7D492A", - "Indexes": [ - "BC10E40AFB79298004CDE51CB065DBDCABA86EC406E3A1CF02CE5F8A9628A2BD" - ] + "Indexes": ["BC10E40AFB79298004CDE51CB065DBDCABA86EC406E3A1CF02CE5F8A9628A2BD"] } }, { @@ -2621,9 +2587,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "A00CD19C13A5CFA3FECB409D42B38017C07A4AEAE05A7A00347DDA17199BA683", - "Indexes": [ - "E49318D6DF22411C3F35581B1D28297A36E47F68B45F36A587C156E6E43CE0A6" - ] + "Indexes": ["E49318D6DF22411C3F35581B1D28297A36E47F68B45F36A587C156E6E43CE0A6"] } }, { @@ -2670,9 +2634,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "A39F044D860C5B5846AA7E0FAAD44DC8897F0A62B2F628AA073B21B3EC146010", - "Indexes": [ - "CD34D8FF7C656B66E2298DB420C918FE27DFFF2186AC8D1785D8CBF2C6BC3488" - ] + "Indexes": ["CD34D8FF7C656B66E2298DB420C918FE27DFFF2186AC8D1785D8CBF2C6BC3488"] } }, { @@ -2721,9 +2683,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "A7E461C6DC98F472991FDE51FADDC0082D755F553F5849875D554B52624EF1C3", - "Indexes": [ - "116C6D5E5C6C59C9C5362B84CB9DD30BD3D4B7CB98CE993D49C068323BF19747" - ] + "Indexes": ["116C6D5E5C6C59C9C5362B84CB9DD30BD3D4B7CB98CE993D49C068323BF19747"] } }, { @@ -2757,9 +2717,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "AA539C8EECE0A0CFF0DBF3BFACD6B42CD4421715428AD90B034091BD3C721038", - "Indexes": [ - "72307CB57E53604A0C50E653AB10E386F3835460B5585B70CB7F668C1E04AC8B" - ] + "Indexes": ["72307CB57E53604A0C50E653AB10E386F3835460B5585B70CB7F668C1E04AC8B"] } }, { @@ -3819,9 +3777,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "D4A00D9B3452C7F93C5F0531FA8FFB4599FEEC405CA803FBEFE0FA22137D863D", - "Indexes": [ - "C1C5FB39D6C15C581D822DBAF725EF7EDE40BEC9F93C52398CF5CE9F64154D6C" - ] + "Indexes": ["C1C5FB39D6C15C581D822DBAF725EF7EDE40BEC9F93C52398CF5CE9F64154D6C"] } }, { @@ -3831,9 +3787,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "D4B68B54869E428428078E1045B8BB66C24DD101DB3FCCBB099929B3B63BCB40", - "Indexes": [ - "9A551971E78FE2FB80D930A77EA0BAC2139A49D6BEB98406427C79F52A347A09" - ] + "Indexes": ["9A551971E78FE2FB80D930A77EA0BAC2139A49D6BEB98406427C79F52A347A09"] } }, { @@ -3908,9 +3862,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "DD23E2C60C9BC58180AC6EA7C668233EC51A0947E42FD1FAD4F5FBAED9698D95", - "Indexes": [ - "908D554AA0D29F660716A3EE65C61DD886B744DDF60DE70E6B16EADB770635DB" - ] + "Indexes": ["908D554AA0D29F660716A3EE65C61DD886B744DDF60DE70E6B16EADB770635DB"] } }, { @@ -4061,9 +4013,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "E2EC9E1BC7B4667B7A5F2F68857F6E6A478A09B5BB4F99E09F694437C4152DED", - "Indexes": [ - "65492B9F30F1CBEA168509128EB8619BAE02A7A7A4725FF3F8DAA70FA707A26E" - ] + "Indexes": ["65492B9F30F1CBEA168509128EB8619BAE02A7A7A4725FF3F8DAA70FA707A26E"] } }, { @@ -4240,9 +4190,7 @@ "IndexPrevious": "0000000000000002", "Flags": 0, "RootIndex": "8E92E688A132410427806A734DF6154B7535E439B72DECA5E4BC7CE17135C5A4", - "Indexes": [ - "73E075E64CA5E7CE60FFCD5359C1D730EDFFEE7C4D992760A87DF7EA0A34E40F" - ] + "Indexes": ["73E075E64CA5E7CE60FFCD5359C1D730EDFFEE7C4D992760A87DF7EA0A34E40F"] } }, { @@ -4346,9 +4294,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "F774E0321809251174AC85531606FB46B75EEF9F842F9697531AA535D3D0C000", - "Indexes": [ - "D1CB738BD08AC36DCB77191DB87C6E40FA478B86503371ED497F30931D7F4F52" - ], + "Indexes": ["D1CB738BD08AC36DCB77191DB87C6E40FA478B86503371ED497F30931D7F4F52"], "TakerPaysIssuer": "E8ACFC6B5EF4EA0601241525375162F43C2FF285" } }, @@ -4409,9 +4355,7 @@ "LedgerEntryType": "DirectoryNode", "Flags": 0, "RootIndex": "F95F6D3A1EF7981E5CA4D5AEC4DA63392B126C76469735BCCA26150A1AF6D9C3", - "Indexes": [ - "CAD951AB279A749AE648FD1DFF56C021BD66E36187022E772C31FE52106CB13B" - ] + "Indexes": ["CAD951AB279A749AE648FD1DFF56C021BD66E36187022E772C31FE52106CB13B"] } }, { @@ -4491,36 +4435,239 @@ } } ], - "transactions": [ + "transactions": [{ + "binary": "1200002200000000240000003E6140000002540BE40068400000000000000A7321034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E74473045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F17962646398114550FC62003E785DC231A1058A05E56E3F09CF4E68314D4CC8AB5B21D86A82C3E9E8D0ECF2404B77FECBA", + "json": { + "Account": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + "Destination": "rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj", + "TransactionType": "Payment", + "TxnSignature": "3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639", + "SigningPubKey": "034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E", + "Amount": "10000000000", + "Fee": "10", + "Flags": 0, + "Sequence": 62 + } + }, + { + "binary": "12002315000A2200000000240015DAE161400000000000271068400000000000000A6BD5838D7EA4C680000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440B3154D968314FCEB58001E1B0C3A4CFB33DF9FF6C73207E5EAEB9BD07E2747672168E1A2786D950495C38BD8DEE3391BF45F3008DD36F4B12E7C07D82CA5250E8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMCreate", + "TxnSignature": "B3154D968314FCEB58001E1B0C3A4CFB33DF9FF6C73207E5EAEB9BD07E2747672168E1A2786D950495C38BD8DEE3391BF45F3008DD36F4B12E7C07D82CA5250E", + "Amount": "10000", + "Amount2": { + "currency": "ETH", + "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", + "value": "10000" + }, + "TradingFee": 10, + "Fee": "10", + "Flags": 0, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8" + } + }, { - "binary": "1200002200000000240000003E6140000002540BE40068400000000000000A7321034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E74473045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F17962646398114550FC62003E785DC231A1058A05E56E3F09CF4E68314D4CC8AB5B21D86A82C3E9E8D0ECF2404B77FECBA", + "binary": "1200242200010000240015DAE168400000000000000A6019D5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B874408073C588E7EF672DD171E414638D9AF8DBE9A1359E030DE3E1C9AA6A38A2CE9E138CB56482BB844F7228D48B1E4AD7D09BB7E9F639C115958EEEA374749CA00B8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", "json": { - "Account": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", - "Destination": "rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj", - "TransactionType": "Payment", - "TxnSignature": "3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639", - "SigningPubKey": "034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E", - "Amount": "10000000000", + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "TxnSignature": "8073C588E7EF672DD171E414638D9AF8DBE9A1359E030DE3E1C9AA6A38A2CE9E138CB56482BB844F7228D48B1E4AD7D09BB7E9F639C115958EEEA374749CA00B", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "LPTokenOut": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, "Fee": "10", - "Flags": 0, - "Sequence": 62 + "Flags": 65536, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8" } - } - ], - "ledgerData": [ + }, { - "binary": "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D521276CDE21276CE60A00", + "binary": "1200242200080000240015DAE16140000000000003E868400000000000000A7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8744096CA066F42871C55088D2758D64148921B1ACAA5C6C648D0F7D675BBF47F87DF711F17C5BD172666D5AEC257520C587A849A6E063345609D91E121A78816EB048114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", "json": { - "account_hash": "3B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D5", - "close_flags": 0, - "close_time": 556231910, - "close_time_resolution": 10, - "ledger_index": 32052277, - "parent_close_time": 556231902, - "parent_hash": "EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6", - "total_coins": "99994494362043555", - "transaction_hash": "DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F87" + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Fee": "10", + "Flags": 524288, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "96CA066F42871C55088D2758D64148921B1ACAA5C6C648D0F7D675BBF47F87DF711F17C5BD172666D5AEC257520C587A849A6E063345609D91E121A78816EB04" } + }, + { + "binary": "1200242200100000240015DAE16140000000000003E868400000000000000A6BD511C37937E080000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440FC22B16A098C236ED7EDB3EBC983026DFD218A03C8BAA848F3E1D5389D5B8B00473C1178C5BA257BFA2DCD433C414690A430A5CFD71C1C0A7F7BF725EC1759018114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Amount2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", "value": "500"}, + "Fee": "10", + "Flags": 1048576, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "FC22B16A098C236ED7EDB3EBC983026DFD218A03C8BAA848F3E1D5389D5B8B00473C1178C5BA257BFA2DCD433C414690A430A5CFD71C1C0A7F7BF725EC175901" + } + }, + { + "binary": "1200242200200000240015DAE16140000000000003E868400000000000000A6019D5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440117CF90F9B113AD3BD638B6DB63562B37C287D5180F278B3CCF58FC14A5BAEE98307EA0F6DFE19E2FBA887C92955BA5D1A04F92ADAAEB309DE89C3610D074C098114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "LPTokenOut": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 2097152, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "117CF90F9B113AD3BD638B6DB63562B37C287D5180F278B3CCF58FC14A5BAEE98307EA0F6DFE19E2FBA887C92955BA5D1A04F92ADAAEB309DE89C3610D074C09" + } + }, + { + "binary": "1200242200400000240015DAE16140000000000003E868400000000000000A601B40000000000000197321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B874405E51EBC6B52A7C3BA5D0AE2FC8F62E779B80182009B3108A87AB6D770D68F56053C193DB0640128E4765565970625B1E2878E116AC854E6DED412202CCDE0B0D8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "EPrice": "25", + "Fee": "10", + "Flags": 4194304, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "5E51EBC6B52A7C3BA5D0AE2FC8F62E779B80182009B3108A87AB6D770D68F56053C193DB0640128E4765565970625B1E2878E116AC854E6DED412202CCDE0B0D" + } + }, + { + "binary": "1200252200010000240015DAE168400000000000000A601AD5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B874409D4F41FC452526C0AD17191959D9B6D04A3C73B3A6C29E0F34C8459675A83A7A7D6E3021390EC8C9BE6C93E11C167E12016465E523F64F9EB3194B0A52E418028114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "LPTokenIn": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 65536, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "9D4F41FC452526C0AD17191959D9B6D04A3C73B3A6C29E0F34C8459675A83A7A7D6E3021390EC8C9BE6C93E11C167E12016465E523F64F9EB3194B0A52E41802" + } + }, + { + "binary": "1200252200080000240015DAE16140000000000003E868400000000000000A7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440E2C60D56C337D6D73E4B7D53579C93C666605494E82A89DD58CFDE79E2A4866BCF52370A2146877A2EF748E98168373710001133A51B645D89491849079035018114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Fee": "10", + "Flags": 524288, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "E2C60D56C337D6D73E4B7D53579C93C666605494E82A89DD58CFDE79E2A4866BCF52370A2146877A2EF748E98168373710001133A51B645D8949184907903501" + } + }, + { + "binary": "1200252200100000240015DAE16140000000000003E868400000000000000A6BD511C37937E080000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440D2FCD7D03E53358BC6188BA88A7BA4FF2519B639C3B5C0EBCBDCB704426CA2837111430E92A6003D1CD0D81C63682C74839320539EC4F89B82AA5607714952028114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Amount2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", "value": "500"}, + "Fee": "10", + "Flags": 1048576, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "D2FCD7D03E53358BC6188BA88A7BA4FF2519B639C3B5C0EBCBDCB704426CA2837111430E92A6003D1CD0D81C63682C74839320539EC4F89B82AA560771495202" + } + }, + { + "binary": "1200252200200000240015DAE16140000000000003E868400000000000000A601AD5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8744042DA5620E924E2D2059BBB4E0C4F03244140ACED93B543136FEEDF802165F814D09F45C7E2A4618468442516F4712A23B1D3332D5DBDBAE830337F39F259C90F8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "LPTokenIn": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 2097152, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "42DA5620E924E2D2059BBB4E0C4F03244140ACED93B543136FEEDF802165F814D09F45C7E2A4618468442516F4712A23B1D3332D5DBDBAE830337F39F259C90F" + } + }, + { + "binary": "1200252200400000240015DAE16140000000000003E868400000000000000A601B40000000000000197321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8744045BCEE5A12E5F5F1FB085A24F2F7FD962BBCB0D89A44A5319E3F7E3799E1870341880B6F684132971DDDF2E6B15356B3F407962D6D4E8DE10989F3B16E3CB90D8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "EPrice": "25", + "Fee": "10", + "Flags": 4194304, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "45BCEE5A12E5F5F1FB085A24F2F7FD962BBCB0D89A44A5319E3F7E3799E1870341880B6F684132971DDDF2E6B15356B3F407962D6D4E8DE10989F3B16E3CB90D" + } + }, + { + "binary": "1200272200000000240015DAE168400000000000000A6CD4C8E1BC9BF04000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0486DD4CC6F3B40B6C000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440F8EAAFB5EC1A69275167589969F0B9764BACE6BC8CC81482C2FC5ACCE691EDBD0D88D141137B1253BB1B9AC90A8A52CB37F5B6F7E1028B06DD06F91BE06F5A0F8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653AF019E01B81149A91957F8F16BC57F3F200CD8C98375BF1791586E1F10318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMBid", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "AuthAccounts": [{"AuthAccount": {"Account": "rEaHTti4HZsMBpxTAF4ncWxkcdqDh1h6P7"}}], + "BidMax": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "35"}, + "BidMin": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "25"}, + "Fee": "10", + "Flags": 0, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "F8EAAFB5EC1A69275167589969F0B9764BACE6BC8CC81482C2FC5ACCE691EDBD0D88D141137B1253BB1B9AC90A8A52CB37F5B6F7E1028B06DD06F91BE06F5A0F" + } + }, + { + "binary": "1200261500EA2200000000240015DAE168400000000000000A7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440BC2F6E76969E3747E9BDE183C97573B086212F09D5387460E6EE2F32953E85EAEB9618FBBEF077276E30E59D619FCF7C7BDCDDDD9EB94D7CE1DD5CE9246B21078114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMVote", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "TradingFee": 234, + "Fee": "10", + "Flags": 0, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "BC2F6E76969E3747E9BDE183C97573B086212F09D5387460E6EE2F32953E85EAEB9618FBBEF077276E30E59D619FCF7C7BDCDDDD9EB94D7CE1DD5CE9246B2107" + } + }], + "ledgerData": [{ + "binary": "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D521276CDE21276CE60A00", + "json": { + "account_hash": "3B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D5", + "close_flags": 0, + "close_time": 556231910, + "close_time_resolution": 10, + "ledger_index": 32052277, + "parent_close_time": 556231902, + "parent_hash": "EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6", + "total_coins": "99994494362043555", + "transaction_hash": "DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F87" } - ] -} + }] +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/resources/data-driven-tests.json b/xrpl4j-core/src/test/resources/data-driven-tests.json index ed7647ea0..5e82b5a00 100644 --- a/xrpl4j-core/src/test/resources/data-driven-tests.json +++ b/xrpl4j-core/src/test/resources/data-driven-tests.json @@ -952,9 +952,7 @@ [ "TransactionType", { - "binary": [ - "0007" - ], + "binary": ["0007"], "json": "OfferCreate", "field_header": "12" } @@ -962,9 +960,7 @@ [ "Flags", { - "binary": [ - "00000000" - ], + "binary": ["00000000"], "json": 0, "field_header": "22" } @@ -972,9 +968,7 @@ [ "Sequence", { - "binary": [ - "000017B4" - ], + "binary": ["000017B4"], "json": 6068, "field_header": "24" } @@ -982,9 +976,7 @@ [ "Expiration", { - "binary": [ - "535A8CF1" - ], + "binary": ["535A8CF1"], "json": 1398443249, "field_header": "2A" } @@ -992,9 +984,7 @@ [ "TakerPays", { - "binary": [ - "4000000006084340" - ], + "binary": ["4000000006084340"], "json": "101204800", "field_header": "64" } @@ -1018,9 +1008,7 @@ [ "Fee", { - "binary": [ - "400000000000000C" - ], + "binary": ["400000000000000C"], "json": "12", "field_header": "68" } @@ -1028,9 +1016,7 @@ [ "Account", { - "binary": [ - "AD6E583D47F90F29FD8B23225E6F905602B0292E" - ], + "binary": ["AD6E583D47F90F29FD8B23225E6F905602B0292E"], "vl_length": "14", "json": "rGFpans8aW7XZNEcNky6RHKyEdLvXPMnUn", "field_header": "81" @@ -1058,9 +1044,7 @@ [ "TransactionType", { - "binary": [ - "0007" - ], + "binary": ["0007"], "json": "OfferCreate", "field_header": "12" } @@ -1068,9 +1052,7 @@ [ "Flags", { - "binary": [ - "00000000" - ], + "binary": ["00000000"], "json": 0, "field_header": "22" } @@ -1078,9 +1060,7 @@ [ "Sequence", { - "binary": [ - "00005124" - ], + "binary": ["00005124"], "json": 20772, "field_header": "24" } @@ -1088,9 +1068,7 @@ [ "LastLedgerSequence", { - "binary": [ - "005EE8EC" - ], + "binary": ["005EE8EC"], "json": 6220012, "field_header": "201B" } @@ -1098,9 +1076,7 @@ [ "TakerPays", { - "binary": [ - "4000000000140251" - ], + "binary": ["4000000000140251"], "json": "1311313", "field_header": "64" } @@ -1124,9 +1100,7 @@ [ "Fee", { - "binary": [ - "400000000000000C" - ], + "binary": ["400000000000000C"], "json": "12", "field_header": "68" } @@ -1134,9 +1108,7 @@ [ "Account", { - "binary": [ - "D0B32295596E50017E246FE85FC5982A1BD89CE4" - ], + "binary": ["D0B32295596E50017E246FE85FC5982A1BD89CE4"], "vl_length": "14", "json": "rLpW9Reyn9YqZ8mxbq8nviXSp4TnHafVJQ", "field_header": "81" @@ -1150,14 +1122,10 @@ "TakerPays": "223174650", "Account": "rPk2dXr27rMw9G5Ej9ad2Tt7RJzGy8ycBp", "TransactionType": "OfferCreate", - "Memos": [ - { - "Memo": { - "MemoType": "584D4D2076616C7565", - "MemoData": "322E3230393635" - } - } - ], + "Memos": [{"Memo": { + "MemoType": "584D4D2076616C7565", + "MemoData": "322E3230393635" + }}], "Fee": "15", "OfferSequence": 1002, "TakerGets": { @@ -1173,9 +1141,7 @@ [ "TransactionType", { - "binary": [ - "0007" - ], + "binary": ["0007"], "json": "OfferCreate", "field_header": "12" } @@ -1183,9 +1149,7 @@ [ "Flags", { - "binary": [ - "00080000" - ], + "binary": ["00080000"], "json": 524288, "field_header": "22" } @@ -1193,9 +1157,7 @@ [ "Sequence", { - "binary": [ - "000003EB" - ], + "binary": ["000003EB"], "json": 1003, "field_header": "24" } @@ -1203,9 +1165,7 @@ [ "OfferSequence", { - "binary": [ - "000003EA" - ], + "binary": ["000003EA"], "json": 1002, "field_header": "2019" } @@ -1213,9 +1173,7 @@ [ "LastLedgerSequence", { - "binary": [ - "005EE967" - ], + "binary": ["005EE967"], "json": 6220135, "field_header": "201B" } @@ -1223,9 +1181,7 @@ [ "TakerPays", { - "binary": [ - "400000000D4D5FFA" - ], + "binary": ["400000000D4D5FFA"], "json": "223174650", "field_header": "64" } @@ -1249,9 +1205,7 @@ [ "Fee", { - "binary": [ - "400000000000000F" - ], + "binary": ["400000000000000F"], "json": "15", "field_header": "68" } @@ -1259,9 +1213,7 @@ [ "Account", { - "binary": [ - "F990B9E746546554A7B50A5E013BCB57095C6BB8" - ], + "binary": ["F990B9E746546554A7B50A5E013BCB57095C6BB8"], "vl_length": "14", "json": "rPk2dXr27rMw9G5Ej9ad2Tt7RJzGy8ycBp", "field_header": "81" @@ -1280,14 +1232,10 @@ "322E3230393635", "E1" ], - "json": [ - { - "Memo": { - "MemoType": "584D4D2076616C7565", - "MemoData": "322E3230393635" - } - } - ], + "json": [{"Memo": { + "MemoType": "584D4D2076616C7565", + "MemoData": "322E3230393635" + }}], "field_header": "F9" } ] @@ -1312,9 +1260,7 @@ [ "TransactionType", { - "binary": [ - "0007" - ], + "binary": ["0007"], "json": "OfferCreate", "field_header": "12" } @@ -1322,9 +1268,7 @@ [ "Sequence", { - "binary": [ - "00080917" - ], + "binary": ["00080917"], "json": 526615, "field_header": "24" } @@ -1332,9 +1276,7 @@ [ "OfferSequence", { - "binary": [ - "000808DA" - ], + "binary": ["000808DA"], "json": 526554, "field_header": "2019" } @@ -1358,9 +1300,7 @@ [ "TakerGets", { - "binary": [ - "400000003C3945C2" - ], + "binary": ["400000003C3945C2"], "json": "1010386370", "field_header": "65" } @@ -1368,9 +1308,7 @@ [ "Fee", { - "binary": [ - "4000000000000032" - ], + "binary": ["4000000000000032"], "json": "50", "field_header": "68" } @@ -1378,9 +1316,7 @@ [ "Account", { - "binary": [ - "F4141D8B4EF33BC3EE224088CA418DFCD2847193" - ], + "binary": ["F4141D8B4EF33BC3EE224088CA418DFCD2847193"], "vl_length": "14", "json": "rPEZyTnSyQyXBCwMVYyaafSVPL8oMtfG6a", "field_header": "81" @@ -1407,22 +1343,16 @@ }, "Flags": 0, "Sequence": 6, - "Paths": [ - [ - { - "account": "razqQKzJRdB4UxFPWf5NEpEG3WMkmwgcXA" - } - ] - ], + "Paths": [[{ + "account": "razqQKzJRdB4UxFPWf5NEpEG3WMkmwgcXA" + }]], "DestinationTag": 736049272 }, "fields": [ [ "TransactionType", { - "binary": [ - "0000" - ], + "binary": ["0000"], "json": "Payment", "field_header": "12" } @@ -1430,9 +1360,7 @@ [ "Flags", { - "binary": [ - "00000000" - ], + "binary": ["00000000"], "json": 0, "field_header": "22" } @@ -1440,9 +1368,7 @@ [ "Sequence", { - "binary": [ - "00000006" - ], + "binary": ["00000006"], "json": 6, "field_header": "24" } @@ -1450,9 +1376,7 @@ [ "DestinationTag", { - "binary": [ - "2BDF3878" - ], + "binary": ["2BDF3878"], "json": 736049272, "field_header": "2E" } @@ -1476,9 +1400,7 @@ [ "Fee", { - "binary": [ - "400000000000000C" - ], + "binary": ["400000000000000C"], "json": "12", "field_header": "68" } @@ -1502,9 +1424,7 @@ [ "Account", { - "binary": [ - "B53847FA45E828BF9A52E38F7FB39E363493CE8B" - ], + "binary": ["B53847FA45E828BF9A52E38F7FB39E363493CE8B"], "vl_length": "14", "json": "rHXUjUtk5eiPFYpg27izxHeZ1t4x835Ecn", "field_header": "81" @@ -1513,9 +1433,7 @@ [ "Destination", { - "binary": [ - "EE39E6D05CFD6A90DAB700A1D70149ECEE29DFEC" - ], + "binary": ["EE39E6D05CFD6A90DAB700A1D70149ECEE29DFEC"], "vl_length": "14", "json": "r45dBj4S3VvMMYXxr9vHX4Z4Ma6ifPMCkK", "field_header": "83" @@ -1529,13 +1447,9 @@ "41C8BE2C0A6AA17471B9F6D0AF92AAB1C94D5A25", "00" ], - "json": [ - [ - { - "account": "razqQKzJRdB4UxFPWf5NEpEG3WMkmwgcXA" - } - ] - ], + "json": [[{ + "account": "razqQKzJRdB4UxFPWf5NEpEG3WMkmwgcXA" + }]], "field_header": "0112" } ] @@ -1565,9 +1479,7 @@ [ "TransactionType", { - "binary": [ - "0000" - ], + "binary": ["0000"], "json": "Payment", "field_header": "12" } @@ -1575,9 +1487,7 @@ [ "Flags", { - "binary": [ - "80000000" - ], + "binary": ["80000000"], "json": 2147483648, "field_header": "22" } @@ -1585,9 +1495,7 @@ [ "Sequence", { - "binary": [ - "000054C7" - ], + "binary": ["000054C7"], "json": 21703, "field_header": "24" } @@ -1611,9 +1519,7 @@ [ "Fee", { - "binary": [ - "400000000000000A" - ], + "binary": ["400000000000000A"], "json": "10", "field_header": "68" } @@ -1637,9 +1543,7 @@ [ "Account", { - "binary": [ - "F7B414E9D25EE050553D8A0BB27202F4249AD328" - ], + "binary": ["F7B414E9D25EE050553D8A0BB27202F4249AD328"], "vl_length": "14", "json": "rP2jdgJhtY1pwDJQEMLfCixesg4cw8HcrW", "field_header": "81" @@ -1648,9 +1552,7 @@ [ "Destination", { - "binary": [ - "B83EB506BBE5BCF3E89C638FDB185B1DEAC96584" - ], + "binary": ["B83EB506BBE5BCF3E89C638FDB185B1DEAC96584"], "vl_length": "14", "json": "rHoUTGMxWKbrTTF8tpAjysjpu8PWrbt1Wx", "field_header": "83" @@ -1673,9 +1575,7 @@ [ "TransactionType", { - "binary": [ - "0000" - ], + "binary": ["0000"], "json": "Payment", "field_header": "12" } @@ -1683,9 +1583,7 @@ [ "Flags", { - "binary": [ - "00000000" - ], + "binary": ["00000000"], "json": 0, "field_header": "22" } @@ -1693,9 +1591,7 @@ [ "Sequence", { - "binary": [ - "00000002" - ], + "binary": ["00000002"], "json": 2, "field_header": "24" } @@ -1703,9 +1599,7 @@ [ "Amount", { - "binary": [ - "40000000017D7840" - ], + "binary": ["40000000017D7840"], "json": "25000000", "field_header": "61" } @@ -1713,9 +1607,7 @@ [ "Fee", { - "binary": [ - "400000000000000A" - ], + "binary": ["400000000000000A"], "json": "10", "field_header": "68" } @@ -1723,9 +1615,7 @@ [ "Account", { - "binary": [ - "5CCB151F6E9D603F394AE778ACF10D3BECE874F6" - ], + "binary": ["5CCB151F6E9D603F394AE778ACF10D3BECE874F6"], "vl_length": "14", "json": "r9TeThyi5xiuUUrFjtPKZiHcDxs7K9H6Rb", "field_header": "81" @@ -1734,9 +1624,7 @@ [ "Destination", { - "binary": [ - "E851BBBE79E328E43D68F43445368133DF5FBA5A" - ], + "binary": ["E851BBBE79E328E43D68F43445368133DF5FBA5A"], "vl_length": "14", "json": "r4BPgS7DHebQiU31xWELvZawwSG2fSPJ7C", "field_header": "83" @@ -1760,9 +1648,7 @@ [ "TransactionType", { - "binary": [ - "0000" - ], + "binary": ["0000"], "json": "Payment", "field_header": "12" } @@ -1770,9 +1656,7 @@ [ "Flags", { - "binary": [ - "00000000" - ], + "binary": ["00000000"], "json": 0, "field_header": "22" } @@ -1780,9 +1664,7 @@ [ "Sequence", { - "binary": [ - "00000090" - ], + "binary": ["00000090"], "json": 144, "field_header": "24" } @@ -1790,9 +1672,7 @@ [ "LastLedgerSequence", { - "binary": [ - "005EE9BA" - ], + "binary": ["005EE9BA"], "json": 6220218, "field_header": "201B" } @@ -1800,9 +1680,7 @@ [ "Amount", { - "binary": [ - "4000000000030D40" - ], + "binary": ["4000000000030D40"], "json": "200000", "field_header": "61" } @@ -1810,9 +1688,7 @@ [ "Fee", { - "binary": [ - "400000000000000F" - ], + "binary": ["400000000000000F"], "json": "15", "field_header": "68" } @@ -1820,9 +1696,7 @@ [ "Account", { - "binary": [ - "AA1BD19D9E87BE8069FDBF6843653C43837C03C6" - ], + "binary": ["AA1BD19D9E87BE8069FDBF6843653C43837C03C6"], "vl_length": "14", "json": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", "field_header": "81" @@ -1831,9 +1705,7 @@ [ "Destination", { - "binary": [ - "67FE6EC28E0464DD24FB2D62A492AAC697CFAD02" - ], + "binary": ["67FE6EC28E0464DD24FB2D62A492AAC697CFAD02"], "vl_length": "14", "json": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", "field_header": "83" @@ -1857,9 +1729,7 @@ [ "TransactionType", { - "binary": [ - "0000" - ], + "binary": ["0000"], "json": "Payment", "field_header": "12" } @@ -1867,9 +1737,7 @@ [ "Flags", { - "binary": [ - "00000000" - ], + "binary": ["00000000"], "json": 0, "field_header": "22" } @@ -1877,9 +1745,7 @@ [ "Sequence", { - "binary": [ - "00000001" - ], + "binary": ["00000001"], "json": 1, "field_header": "24" } @@ -1887,9 +1753,7 @@ [ "DestinationTag", { - "binary": [ - "F72D50CA" - ], + "binary": ["F72D50CA"], "json": 4146942154, "field_header": "2E" } @@ -1897,9 +1761,7 @@ [ "Amount", { - "binary": [ - "40000000017D7840" - ], + "binary": ["40000000017D7840"], "json": "25000000", "field_header": "61" } @@ -1907,9 +1769,7 @@ [ "Fee", { - "binary": [ - "400000000000000C" - ], + "binary": ["400000000000000C"], "json": "12", "field_header": "68" } @@ -1917,9 +1777,7 @@ [ "Account", { - "binary": [ - "E851BBBE79E328E43D68F43445368133DF5FBA5A" - ], + "binary": ["E851BBBE79E328E43D68F43445368133DF5FBA5A"], "vl_length": "14", "json": "r4BPgS7DHebQiU31xWELvZawwSG2fSPJ7C", "field_header": "81" @@ -1928,9 +1786,7 @@ [ "Destination", { - "binary": [ - "76DAC5E814CD4AA74142C3AB45E69A900E637AA2" - ], + "binary": ["76DAC5E814CD4AA74142C3AB45E69A900E637AA2"], "vl_length": "14", "json": "rBqSFEFg2B6GBMobtxnU1eLA1zbNC9NDGM", "field_header": "83" @@ -1953,9 +1809,7 @@ [ "TransactionType", { - "binary": [ - "0000" - ], + "binary": ["0000"], "json": "Payment", "field_header": "12" } @@ -1963,9 +1817,7 @@ [ "SourceTag", { - "binary": [ - "000A34F8" - ], + "binary": ["000A34F8"], "json": 668920, "field_header": "23" } @@ -1973,9 +1825,7 @@ [ "Sequence", { - "binary": [ - "0000888A" - ], + "binary": ["0000888A"], "json": 34954, "field_header": "24" } @@ -1983,9 +1833,7 @@ [ "Amount", { - "binary": [ - "400000000007A120" - ], + "binary": ["400000000007A120"], "json": "500000", "field_header": "61" } @@ -1993,9 +1841,7 @@ [ "Fee", { - "binary": [ - "4000000000000014" - ], + "binary": ["4000000000000014"], "json": "20", "field_header": "68" } @@ -2003,9 +1849,7 @@ [ "Account", { - "binary": [ - "08F41F116A1F60D60296B16907F0A041BF106197" - ], + "binary": ["08F41F116A1F60D60296B16907F0A041BF106197"], "vl_length": "14", "json": "rFLiPGytDEwC5heoqFcFAZoqPPmKBzX1o", "field_header": "81" @@ -2014,9 +1858,7 @@ [ "Destination", { - "binary": [ - "6E2F0455C46CF5DF61A1E58419A89D45459045EA" - ], + "binary": ["6E2F0455C46CF5DF61A1E58419A89D45459045EA"], "vl_length": "14", "json": "rBsbetvMYuMkEeHZYizPMkpveCVH8EVQYd", "field_header": "83" @@ -2039,14 +1881,10 @@ "SendMax": "3267350000", "Flags": 0, "Sequence": 10, - "Paths": [ - [ - { - "currency": "BTC", - "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q" - } - ] - ], + "Paths": [[{ + "currency": "BTC", + "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q" + }]], "InvoiceID": "342B8D16BEE494D169034AFF0908FDE35874A38E548D4CEC8DFC5C49E9A33B76", "DestinationTag": 1403334172 }, @@ -2054,9 +1892,7 @@ [ "TransactionType", { - "binary": [ - "0000" - ], + "binary": ["0000"], "json": "Payment", "field_header": "12" } @@ -2064,9 +1900,7 @@ [ "Flags", { - "binary": [ - "00000000" - ], + "binary": ["00000000"], "json": 0, "field_header": "22" } @@ -2074,9 +1908,7 @@ [ "Sequence", { - "binary": [ - "0000000A" - ], + "binary": ["0000000A"], "json": 10, "field_header": "24" } @@ -2084,9 +1916,7 @@ [ "DestinationTag", { - "binary": [ - "53A52E1C" - ], + "binary": ["53A52E1C"], "json": 1403334172, "field_header": "2E" } @@ -2094,9 +1924,7 @@ [ "InvoiceID", { - "binary": [ - "342B8D16BEE494D169034AFF0908FDE35874A38E548D4CEC8DFC5C49E9A33B76" - ], + "binary": ["342B8D16BEE494D169034AFF0908FDE35874A38E548D4CEC8DFC5C49E9A33B76"], "json": "342B8D16BEE494D169034AFF0908FDE35874A38E548D4CEC8DFC5C49E9A33B76", "field_header": "5011" } @@ -2120,9 +1948,7 @@ [ "Fee", { - "binary": [ - "400000000000006A" - ], + "binary": ["400000000000006A"], "json": "106", "field_header": "68" } @@ -2130,9 +1956,7 @@ [ "SendMax", { - "binary": [ - "40000000C2BFCDF0" - ], + "binary": ["40000000C2BFCDF0"], "json": "3267350000", "field_header": "69" } @@ -2140,9 +1964,7 @@ [ "Account", { - "binary": [ - "52E0F910686FB449A23BC78C3D4CE564C988C6C0" - ], + "binary": ["52E0F910686FB449A23BC78C3D4CE564C988C6C0"], "vl_length": "14", "json": "r3ZDv3hLmTKwkgAqcXtX2yaMfnhRD3Grjc", "field_header": "81" @@ -2151,9 +1973,7 @@ [ "Destination", { - "binary": [ - "DD39C650A96EDA48334E70CC4A85B8B2E8502CD3" - ], + "binary": ["DD39C650A96EDA48334E70CC4A85B8B2E8502CD3"], "vl_length": "14", "json": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", "field_header": "83" @@ -2168,14 +1988,10 @@ "DD39C650A96EDA48334E70CC4A85B8B2E8502CD3", "00" ], - "json": [ - [ - { - "currency": "BTC", - "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q" - } - ] - ], + "json": [[{ + "currency": "BTC", + "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q" + }]], "field_header": "0112" } ] @@ -2196,9 +2012,7 @@ [ "TransactionType", { - "binary": [ - "0008" - ], + "binary": ["0008"], "json": "OfferCancel", "field_header": "12" } @@ -2206,9 +2020,7 @@ [ "Flags", { - "binary": [ - "00000000" - ], + "binary": ["00000000"], "json": 0, "field_header": "22" } @@ -2216,9 +2028,7 @@ [ "Sequence", { - "binary": [ - "00005121" - ], + "binary": ["00005121"], "json": 20769, "field_header": "24" } @@ -2226,9 +2036,7 @@ [ "OfferSequence", { - "binary": [ - "0000511B" - ], + "binary": ["0000511B"], "json": 20763, "field_header": "2019" } @@ -2236,9 +2044,7 @@ [ "LastLedgerSequence", { - "binary": [ - "005EE8E9" - ], + "binary": ["005EE8E9"], "json": 6220009, "field_header": "201B" } @@ -2246,9 +2052,7 @@ [ "Fee", { - "binary": [ - "400000000000000C" - ], + "binary": ["400000000000000C"], "json": "12", "field_header": "68" } @@ -2256,9 +2060,7 @@ [ "Account", { - "binary": [ - "D0B32295596E50017E246FE85FC5982A1BD89CE4" - ], + "binary": ["D0B32295596E50017E246FE85FC5982A1BD89CE4"], "vl_length": "14", "json": "rLpW9Reyn9YqZ8mxbq8nviXSp4TnHafVJQ", "field_header": "81" @@ -2280,9 +2082,7 @@ [ "TransactionType", { - "binary": [ - "0005" - ], + "binary": ["0005"], "json": "SetRegularKey", "field_header": "12" } @@ -2290,9 +2090,7 @@ [ "Flags", { - "binary": [ - "80000000" - ], + "binary": ["80000000"], "json": 2147483648, "field_header": "22" } @@ -2300,9 +2098,7 @@ [ "Sequence", { - "binary": [ - "00000003" - ], + "binary": ["00000003"], "json": 3, "field_header": "24" } @@ -2310,9 +2106,7 @@ [ "Fee", { - "binary": [ - "400000000000000A" - ], + "binary": ["400000000000000A"], "json": "10", "field_header": "68" } @@ -2320,9 +2114,7 @@ [ "Account", { - "binary": [ - "48E143E2384A1B3C69A412789F2CA3FCE2F65F0B" - ], + "binary": ["48E143E2384A1B3C69A412789F2CA3FCE2F65F0B"], "vl_length": "14", "json": "rfeMWWbSaGqc6Yth2dTetLBeKeUTTfE2pG", "field_header": "81" @@ -2331,9 +2123,7 @@ [ "RegularKey", { - "binary": [ - "48E143E2384A1B3C69A412789F2CA3FCE2F65F0B" - ], + "binary": ["48E143E2384A1B3C69A412789F2CA3FCE2F65F0B"], "vl_length": "14", "json": "rfeMWWbSaGqc6Yth2dTetLBeKeUTTfE2pG", "field_header": "88" @@ -2356,9 +2146,7 @@ [ "TransactionType", { - "binary": [ - "0005" - ], + "binary": ["0005"], "json": "SetRegularKey", "field_header": "12" } @@ -2366,9 +2154,7 @@ [ "Flags", { - "binary": [ - "80000000" - ], + "binary": ["80000000"], "json": 2147483648, "field_header": "22" } @@ -2376,9 +2162,7 @@ [ "Sequence", { - "binary": [ - "000000EE" - ], + "binary": ["000000EE"], "json": 238, "field_header": "24" } @@ -2386,9 +2170,7 @@ [ "LastLedgerSequence", { - "binary": [ - "005EF94C" - ], + "binary": ["005EF94C"], "json": 6224204, "field_header": "201B" } @@ -2396,9 +2178,7 @@ [ "Fee", { - "binary": [ - "400000000000000C" - ], + "binary": ["400000000000000C"], "json": "12", "field_header": "68" } @@ -2406,9 +2186,7 @@ [ "Account", { - "binary": [ - "CB3F392892D0772FF5AD155D8D70404B1DB2ACFE" - ], + "binary": ["CB3F392892D0772FF5AD155D8D70404B1DB2ACFE"], "vl_length": "14", "json": "rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz", "field_header": "81" @@ -2417,9 +2195,7 @@ [ "RegularKey", { - "binary": [ - "F2F9A54D9CEBBE64342B52DE3450FFA0738C8D00" - ], + "binary": ["F2F9A54D9CEBBE64342B52DE3450FFA0738C8D00"], "vl_length": "14", "json": "rP9jbfTepHAHWB4q9YjNkLyaZT15uvexiZ", "field_header": "88" @@ -2445,9 +2221,7 @@ [ "TransactionType", { - "binary": [ - "0014" - ], + "binary": ["0014"], "json": "TrustSet", "field_header": "12" } @@ -2455,9 +2229,7 @@ [ "Flags", { - "binary": [ - "00020000" - ], + "binary": ["00020000"], "json": 131072, "field_header": "22" } @@ -2465,9 +2237,7 @@ [ "Sequence", { - "binary": [ - "0000002C" - ], + "binary": ["0000002C"], "json": 44, "field_header": "24" } @@ -2491,9 +2261,7 @@ [ "Fee", { - "binary": [ - "400000000000000C" - ], + "binary": ["400000000000000C"], "json": "12", "field_header": "68" } @@ -2501,9 +2269,7 @@ [ "Account", { - "binary": [ - "BE6C30732AE33CF2AF3344CE8172A6B9300183E3" - ], + "binary": ["BE6C30732AE33CF2AF3344CE8172A6B9300183E3"], "vl_length": "14", "json": "rJMiz2rCMjZzEMijXNH1exNBryTQEjFd9S", "field_header": "81" @@ -2530,9 +2296,7 @@ [ "TransactionType", { - "binary": [ - "0014" - ], + "binary": ["0014"], "json": "TrustSet", "field_header": "12" } @@ -2540,9 +2304,7 @@ [ "Flags", { - "binary": [ - "80020000" - ], + "binary": ["80020000"], "json": 2147614720, "field_header": "22" } @@ -2550,9 +2312,7 @@ [ "Sequence", { - "binary": [ - "0000002B" - ], + "binary": ["0000002B"], "json": 43, "field_header": "24" } @@ -2560,9 +2320,7 @@ [ "LastLedgerSequence", { - "binary": [ - "005EEAAF" - ], + "binary": ["005EEAAF"], "json": 6220463, "field_header": "201B" } @@ -2586,9 +2344,7 @@ [ "Fee", { - "binary": [ - "400000000000000C" - ], + "binary": ["400000000000000C"], "json": "12", "field_header": "68" } @@ -2596,9 +2352,7 @@ [ "Account", { - "binary": [ - "8353C031DF5AA061A23535E6ABCEEEA23F152B1E" - ], + "binary": ["8353C031DF5AA061A23535E6ABCEEEA23F152B1E"], "vl_length": "14", "json": "rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN", "field_header": "81" @@ -2619,9 +2373,7 @@ [ "TransactionType", { - "binary": [ - "0003" - ], + "binary": ["0003"], "json": "AccountSet", "field_header": "12" } @@ -2629,9 +2381,7 @@ [ "Flags", { - "binary": [ - "00000000" - ], + "binary": ["00000000"], "json": 0, "field_header": "22" } @@ -2639,9 +2389,7 @@ [ "Sequence", { - "binary": [ - "00002966" - ], + "binary": ["00002966"], "json": 10598, "field_header": "24" } @@ -2649,9 +2397,7 @@ [ "Fee", { - "binary": [ - "400000000000000A" - ], + "binary": ["400000000000000A"], "json": "10", "field_header": "68" } @@ -2659,9 +2405,7 @@ [ "Account", { - "binary": [ - "0F3D0C7D2CFAB2EC8295451F0B3CA038E8E9CDCD" - ], + "binary": ["0F3D0C7D2CFAB2EC8295451F0B3CA038E8E9CDCD"], "vl_length": "14", "json": "rpP2GdsQwenNnFPefbXFgiTvEgJWQpq8Rw", "field_header": "81" @@ -2683,9 +2427,7 @@ [ "TransactionType", { - "binary": [ - "0003" - ], + "binary": ["0003"], "json": "AccountSet", "field_header": "12" } @@ -2693,9 +2435,7 @@ [ "Flags", { - "binary": [ - "00000000" - ], + "binary": ["00000000"], "json": 0, "field_header": "22" } @@ -2703,9 +2443,7 @@ [ "Sequence", { - "binary": [ - "00000122" - ], + "binary": ["00000122"], "json": 290, "field_header": "24" } @@ -2713,9 +2451,7 @@ [ "LastLedgerSequence", { - "binary": [ - "005EECD6" - ], + "binary": ["005EECD6"], "json": 6221014, "field_header": "201B" } @@ -2723,9 +2459,7 @@ [ "Fee", { - "binary": [ - "400000000000000F" - ], + "binary": ["400000000000000F"], "json": "15", "field_header": "68" } @@ -2733,9 +2467,7 @@ [ "Account", { - "binary": [ - "ABBD4A3AF95FDFD6D072F11421D8F107CAEA1852" - ], + "binary": ["ABBD4A3AF95FDFD6D072F11421D8F107CAEA1852"], "vl_length": "14", "json": "rGCnJuD31Kx4QGZJ2dX7xoje6T4Zr5s9EB", "field_header": "81" diff --git a/xrpl4j-core/src/test/resources/tx_metadata_fixtures.json.zip b/xrpl4j-core/src/test/resources/tx_metadata_fixtures.json.zip index f119f16af..14d3522c7 100644 Binary files a/xrpl4j-core/src/test/resources/tx_metadata_fixtures.json.zip and b/xrpl4j-core/src/test/resources/tx_metadata_fixtures.json.zip differ diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java index 6b240ac37..53307bcba 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java @@ -22,10 +22,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.given; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.core.Is.is; import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; import org.awaitility.Durations; import org.slf4j.Logger; @@ -44,6 +46,8 @@ import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.crypto.signing.bc.BcDerivedKeySignatureService; import org.xrpl.xrpl4j.crypto.signing.bc.BcSignatureService; +import org.xrpl.xrpl4j.model.client.Finality; +import org.xrpl.xrpl4j.model.client.FinalityStatus; import org.xrpl.xrpl4j.model.client.XrplResult; import org.xrpl.xrpl4j.model.client.accounts.AccountChannelsRequestParams; import org.xrpl.xrpl4j.model.client.accounts.AccountChannelsResult; @@ -54,6 +58,7 @@ import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsRequestParams; import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsResult; import org.xrpl.xrpl4j.model.client.accounts.TrustLine; +import org.xrpl.xrpl4j.model.client.common.LedgerIndex; import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams; import org.xrpl.xrpl4j.model.client.ledger.LedgerResult; @@ -204,6 +209,36 @@ protected void fundAccount(final Address address) { // Ledger Helpers ////////////////////// + protected Finality scanForFinality( + Hash256 transactionHash, + LedgerIndex submittedOnLedgerIndex, + UnsignedInteger lastLedgerSequence, + UnsignedInteger transactionAccountSequence, + Address account + ) { + return given() + .pollInterval(POLL_INTERVAL) + .atMost(Durations.ONE_MINUTE.dividedBy(2)) + .ignoreException(RuntimeException.class) + .await() + .until( + () -> xrplClient.isFinal( + transactionHash, + submittedOnLedgerIndex, + lastLedgerSequence, + transactionAccountSequence, + account + ), + is(equalTo( + Finality.builder() + .finalityStatus(FinalityStatus.VALIDATED_SUCCESS) + .resultCode(TransactionResultCodes.TES_SUCCESS) + .build() + ) + ) + ); + } + protected T scanForResult(Supplier resultSupplier, Predicate condition) { return given() .atMost(Durations.ONE_MINUTE.dividedBy(2)) diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AccountSetIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AccountSetIT.java index 0ea468489..18e2e9378 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AccountSetIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AccountSetIT.java @@ -30,12 +30,17 @@ import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.crypto.signing.bc.BcSignatureService; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; import org.xrpl.xrpl4j.model.flags.AccountRootFlags; import org.xrpl.xrpl4j.model.flags.AccountSetTransactionFlags; +import org.xrpl.xrpl4j.model.ledger.AccountRootObject; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.transactions.AccountSet; import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag; @@ -59,9 +64,12 @@ public void enableAllAndDisableOne() throws JsonRpcClientErrorException, JsonPro AccountInfoResult accountInfo = this.scanForResult( () -> this.getValidatedAccountInfo(keyPair.publicKey().deriveAddress()) ); + assertThat(accountInfo.status()).isNotEmpty().get().isEqualTo("success"); assertThat(accountInfo.accountData().flags().lsfGlobalFreeze()).isEqualTo(false); + assertEntryEqualsAccountInfo(keyPair, accountInfo); + UnsignedInteger sequence = accountInfo.accountData().sequence(); ////////////////////// // Set asfAccountTxnID (no corresponding ledger flag) @@ -384,10 +392,34 @@ void submitAndRetrieveAccountSetWithZeroClearFlagAndSetFlag() assertThat(accountSetTransactionResult.transaction().clearFlag()).isNotEmpty().get().isEqualTo(AccountSetFlag.NONE); } + ////////////////////// // Test Helpers ////////////////////// + private void assertEntryEqualsAccountInfo( + KeyPair keyPair, + AccountInfoResult accountInfo + ) throws JsonRpcClientErrorException { + LedgerEntryResult accountRoot = xrplClient.ledgerEntry( + LedgerEntryRequestParams.accountRoot(keyPair.publicKey().deriveAddress(), LedgerSpecifier.VALIDATED) + ); + + assertThat(accountInfo.accountData()).isEqualTo(accountRoot.node()); + + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(accountRoot.index(), AccountRootObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(accountRoot.node()); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(accountRoot.index(), LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); + } + private void assertSetFlag( final KeyPair keyPair, final UnsignedInteger sequence, @@ -420,12 +452,14 @@ private void assertSetFlag( ///////////////////////// // Validate Account State - this.scanForResult( + AccountInfoResult accountInfo = this.scanForResult( () -> this.getValidatedAccountInfo(keyPair.publicKey().deriveAddress()), accountInfoResult -> { logger.info("AccountInfoResponse Flags: {}", accountInfoResult.accountData().flags()); return accountInfoResult.accountData().flags().isSet(accountRootFlag); }); + + assertEntryEqualsAccountInfo(keyPair, accountInfo); } private void assertClearFlag( @@ -460,11 +494,13 @@ private void assertClearFlag( ///////////////////////// // Validate Account State - this.scanForResult( + AccountInfoResult accountInfo = this.scanForResult( () -> this.getValidatedAccountInfo(keyPair.publicKey().deriveAddress()), accountInfoResult -> { logger.info("AccountInfoResponse Flags: {}", accountInfoResult.accountData().flags()); return !accountInfoResult.accountData().flags().isSet(accountRootFlag); }); + + assertEntryEqualsAccountInfo(keyPair, accountInfo); } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AmmIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AmmIT.java new file mode 100644 index 000000000..b908e9287 --- /dev/null +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AmmIT.java @@ -0,0 +1,504 @@ +package org.xrpl.xrpl4j.tests; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.Test; +import org.junit.jupiter.api.condition.DisabledIf; +import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; +import org.xrpl.xrpl4j.crypto.keys.KeyPair; +import org.xrpl.xrpl4j.crypto.keys.PrivateKey; +import org.xrpl.xrpl4j.crypto.signing.SignatureService; +import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; +import org.xrpl.xrpl4j.model.client.accounts.AccountInfoRequestParams; +import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.accounts.AccountLinesRequestParams; +import org.xrpl.xrpl4j.model.client.accounts.AccountLinesResult; +import org.xrpl.xrpl4j.model.client.accounts.TrustLine; +import org.xrpl.xrpl4j.model.client.amm.AmmInfoAuctionSlot; +import org.xrpl.xrpl4j.model.client.amm.AmmInfoAuthAccount; +import org.xrpl.xrpl4j.model.client.amm.AmmInfoRequestParams; +import org.xrpl.xrpl4j.model.client.amm.AmmInfoResult; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; +import org.xrpl.xrpl4j.model.client.common.TimeUtils; +import org.xrpl.xrpl4j.model.client.fees.FeeResult; +import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.AmmLedgerEntryParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; +import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; +import org.xrpl.xrpl4j.model.flags.AmmDepositFlags; +import org.xrpl.xrpl4j.model.flags.AmmWithdrawFlags; +import org.xrpl.xrpl4j.model.ledger.AmmObject; +import org.xrpl.xrpl4j.model.ledger.AuctionSlot; +import org.xrpl.xrpl4j.model.ledger.AuthAccount; +import org.xrpl.xrpl4j.model.ledger.AuthAccountWrapper; +import org.xrpl.xrpl4j.model.ledger.Issue; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; +import org.xrpl.xrpl4j.model.transactions.AccountSet; +import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag; +import org.xrpl.xrpl4j.model.transactions.AmmBid; +import org.xrpl.xrpl4j.model.transactions.AmmCreate; +import org.xrpl.xrpl4j.model.transactions.AmmDeposit; +import org.xrpl.xrpl4j.model.transactions.AmmVote; +import org.xrpl.xrpl4j.model.transactions.AmmWithdraw; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TradingFee; +import org.xrpl.xrpl4j.model.transactions.TransactionResultCodes; +import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.stream.Collectors; + +@DisabledIf(value = "shouldNotRun", disabledReason = "AmmIT only runs on local rippled node or devnet.") +public class AmmIT extends AbstractIT { + + static boolean shouldNotRun() { + return System.getProperty("useTestnet") != null || + System.getProperty("useClioTestnet") != null; + } + + String xrpl4jCoin = Strings.padEnd(BaseEncoding.base16().encode("xrpl4jCoin".getBytes()), 40, '0'); + + @Test + void depositAndVoteOnTradingFee() throws JsonRpcClientErrorException, JsonProcessingException { + KeyPair issuerKeyPair = createRandomAccountEd25519(); + FeeResult feeResult = xrplClient.fee(); + AmmInfoResult amm = createAmm(issuerKeyPair, feeResult); + KeyPair traderKeyPair = createRandomAccountEd25519(); + + AccountInfoResult traderAccount = scanForResult( + () -> this.getValidatedAccountInfo(traderKeyPair.publicKey().deriveAddress()) + ); + + AccountInfoResult traderAccountAfterDeposit = depositXrp( + issuerKeyPair, + traderKeyPair, + traderAccount, + amm, + signatureService, + feeResult + ); + + TradingFee newTradingFee = TradingFee.ofPercent(BigDecimal.valueOf(0.24)); + AmmVote ammVote = AmmVote.builder() + .account(traderAccount.accountData().account()) + .sequence(traderAccountAfterDeposit.accountData().sequence()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) + .lastLedgerSequence(traderAccount.ledgerIndexSafe().plus(UnsignedInteger.valueOf(8)).unsignedIntegerValue()) + .signingPublicKey(traderKeyPair.publicKey()) + .asset2( + Issue.builder() + .currency(xrpl4jCoin) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build() + ) + .asset(Issue.XRP) + .tradingFee(newTradingFee) + .build(); + + SingleSignedTransaction signedVote = signatureService.sign(traderKeyPair.privateKey(), ammVote); + + SubmitResult voteSubmitResult = xrplClient.submit(signedVote); + assertThat(voteSubmitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + + scanForFinality( + signedVote.hash(), + traderAccount.ledgerIndexSafe(), + ammVote.lastLedgerSequence().get(), + ammVote.sequence(), + traderKeyPair.publicKey().deriveAddress() + ); + + BigDecimal issuerLpTokenBalance = new BigDecimal(xrplClient.accountLines( + AccountLinesRequestParams.builder() + .account(issuerKeyPair.publicKey().deriveAddress()) + .peer(amm.amm().account()) + .ledgerSpecifier(LedgerSpecifier.CURRENT) + .build() + ).lines().stream() + .filter(trustLine -> trustLine.currency().equals(amm.amm().lpToken().currency())) + .findFirst() + .orElseThrow(RuntimeException::new) + .balance()); + + BigDecimal traderLpTokenBalance = new BigDecimal(xrplClient.accountLines( + AccountLinesRequestParams.builder() + .account(traderKeyPair.publicKey().deriveAddress()) + .peer(amm.amm().account()) + .ledgerSpecifier(LedgerSpecifier.CURRENT) + .build() + ).lines().stream() + .filter(trustLine -> trustLine.currency().equals(amm.amm().lpToken().currency())) + .findFirst() + .orElseThrow(RuntimeException::new) + .balance()); + + // Expected trading fee is the weighted average of each vote, where the weight is number of LP tokens held + // by each voter + TradingFee expectedTradingFee = TradingFee.ofPercent( + issuerLpTokenBalance.multiply(amm.amm().tradingFee().bigDecimalValue()).add( + traderLpTokenBalance.multiply(newTradingFee.bigDecimalValue()) + ).divide(issuerLpTokenBalance.add(traderLpTokenBalance), RoundingMode.FLOOR) + .setScale(3, RoundingMode.FLOOR) + ); + + AmmInfoResult ammAfterVote = getAmmInfo(issuerKeyPair); + assertThat(ammAfterVote.amm().tradingFee()).isEqualTo(expectedTradingFee); + } + + @Test + void depositAndBid() throws JsonRpcClientErrorException, JsonProcessingException { + KeyPair issuerKeyPair = createRandomAccountEd25519(); + FeeResult feeResult = xrplClient.fee(); + AmmInfoResult amm = createAmm(issuerKeyPair, feeResult); + KeyPair traderKeyPair = createRandomAccountEd25519(); + KeyPair authAccount1 = createRandomAccountEd25519(); + + AccountInfoResult traderAccount = scanForResult( + () -> this.getValidatedAccountInfo(traderKeyPair.publicKey().deriveAddress()) + ); + + AccountInfoResult traderAccountAfterDeposit = depositXrp( + issuerKeyPair, + traderKeyPair, + traderAccount, + amm, + signatureService, + feeResult + ); + + AmmBid bid = AmmBid.builder() + .account(traderAccount.accountData().account()) + .sequence(traderAccountAfterDeposit.accountData().sequence()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) + .lastLedgerSequence(traderAccount.ledgerIndexSafe().plus(UnsignedInteger.valueOf(8)).unsignedIntegerValue()) + .signingPublicKey(traderKeyPair.publicKey()) + .asset2( + Issue.builder() + .currency(xrpl4jCoin) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build() + ) + .asset(Issue.XRP) + .addAuthAccounts( + AuthAccountWrapper.of(AuthAccount.of(authAccount1.publicKey().deriveAddress())) + ) + .bidMin( + IssuedCurrencyAmount.builder() + .from(amm.amm().lpToken()) + .value("100") + .build() + ) + .build(); + + SingleSignedTransaction signedBid = signatureService.sign(traderKeyPair.privateKey(), bid); + + SubmitResult voteSubmitResult = xrplClient.submit(signedBid); + assertThat(voteSubmitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + + scanForFinality( + signedBid.hash(), + traderAccount.ledgerIndexSafe(), + bid.lastLedgerSequence().get(), + bid.sequence(), + traderKeyPair.publicKey().deriveAddress() + ); + + AmmInfoResult ammAfterBid = getAmmInfo(issuerKeyPair); + + assertThat(ammAfterBid.amm().auctionSlot()).isNotEmpty(); + AmmInfoAuctionSlot auctionSlot = ammAfterBid.amm().auctionSlot().get(); + assertThat(auctionSlot.account()).isEqualTo(traderAccount.accountData().account()); + assertThat(auctionSlot.authAccounts()).asList().extracting("account") + .containsExactly(authAccount1.publicKey().deriveAddress()); + } + + @Test + void depositAndWithdraw() throws JsonRpcClientErrorException, JsonProcessingException { + KeyPair issuerKeyPair = createRandomAccountEd25519(); + FeeResult feeResult = xrplClient.fee(); + AmmInfoResult amm = createAmm(issuerKeyPair, feeResult); + KeyPair traderKeyPair = createRandomAccountEd25519(); + + AccountInfoResult traderAccount = scanForResult( + () -> this.getValidatedAccountInfo(traderKeyPair.publicKey().deriveAddress()) + ); + + AccountInfoResult traderAccountAfterDeposit = depositXrp( + issuerKeyPair, + traderKeyPair, + traderAccount, + amm, + signatureService, + feeResult + ); + + AmmInfoResult ammInfoAfterDeposit = getAmmInfo(issuerKeyPair); + AmmWithdraw withdraw = AmmWithdraw.builder() + .account(traderKeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) + .sequence(traderAccountAfterDeposit.accountData().sequence()) + .lastLedgerSequence( + traderAccountAfterDeposit.ledgerCurrentIndexSafe().plus(UnsignedInteger.valueOf(4)).unsignedIntegerValue() + ) + .signingPublicKey(traderKeyPair.publicKey()) + .asset2( + Issue.builder() + .currency(xrpl4jCoin) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build() + ) + .asset(Issue.XRP) + .amount(XrpCurrencyAmount.ofXrp(BigDecimal.valueOf(90))) + .flags(AmmWithdrawFlags.SINGLE_ASSET) + .build(); + + SingleSignedTransaction signedWithdraw = signatureService.sign(traderKeyPair.privateKey(), withdraw); + + SubmitResult voteSubmitResult = xrplClient.submit(signedWithdraw); + assertThat(voteSubmitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + + scanForFinality( + signedWithdraw.hash(), + traderAccount.ledgerIndexSafe(), + withdraw.lastLedgerSequence().get(), + withdraw.sequence(), + traderKeyPair.publicKey().deriveAddress() + ); + + AmmInfoResult ammAfterWithdraw = getAmmInfo(issuerKeyPair); + assertThat(ammAfterWithdraw.amm().amount()).isInstanceOf(XrpCurrencyAmount.class) + .isEqualTo(((XrpCurrencyAmount) ammInfoAfterDeposit.amm().amount()) + .minus((XrpCurrencyAmount) withdraw.amount().get())); + + AccountInfoResult traderAccountAfterWithdraw = xrplClient.accountInfo( + AccountInfoRequestParams.of(traderKeyPair.publicKey().deriveAddress()) + ); + + assertThat(traderAccountAfterWithdraw.accountData().balance()).isEqualTo( + traderAccountAfterDeposit.accountData().balance() + .minus(withdraw.fee()) + .plus((XrpCurrencyAmount) withdraw.amount().get()) + ); + } + + private AccountInfoResult depositXrp( + KeyPair issuerKeyPair, + KeyPair traderKeyPair, + AccountInfoResult traderAccount, + AmmInfoResult amm, + SignatureService signatureService, + FeeResult feeResult + ) throws JsonRpcClientErrorException, JsonProcessingException { + XrpCurrencyAmount depositAmount = XrpCurrencyAmount.ofXrp(BigDecimal.valueOf(100)); + AmmDeposit deposit = AmmDeposit.builder() + .account(traderAccount.accountData().account()) + .asset2( + Issue.builder() + .currency(xrpl4jCoin) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build() + ) + .asset(Issue.XRP) + .flags(AmmDepositFlags.SINGLE_ASSET) + .amount(depositAmount) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) + .sequence(traderAccount.accountData().sequence()) + .signingPublicKey(traderKeyPair.publicKey()) + .lastLedgerSequence(traderAccount.ledgerIndexSafe().plus(UnsignedInteger.valueOf(4)).unsignedIntegerValue()) + .build(); + + SingleSignedTransaction signedDeposit = signatureService.sign(traderKeyPair.privateKey(), deposit); + SubmitResult submitResult = xrplClient.submit(signedDeposit); + assertThat(submitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + + scanForFinality( + signedDeposit.hash(), + traderAccount.ledgerIndexSafe(), + deposit.lastLedgerSequence().get(), + deposit.sequence(), + traderKeyPair.publicKey().deriveAddress() + ); + + AccountInfoResult traderAccountAfterDeposit = xrplClient.accountInfo( + AccountInfoRequestParams.of(traderAccount.accountData().account()) + ); + + assertThat(traderAccountAfterDeposit.accountData().balance()) + .isEqualTo(traderAccount.accountData().balance().minus(deposit.fee()).minus(depositAmount)); + + AccountLinesResult traderLines = xrplClient.accountLines( + AccountLinesRequestParams.builder() + .account(traderAccount.accountData().account()) + .peer(amm.amm().account()) + .ledgerSpecifier(LedgerSpecifier.CURRENT) + .build() + ); + + assertThat(traderLines.lines()).asList().hasSize(1); + TrustLine lpLine = traderLines.lines().get(0); + assertThat(lpLine.currency()).isEqualTo(amm.amm().lpToken().currency()); + assertThat(new BigDecimal(lpLine.balance())).isGreaterThan(BigDecimal.ZERO); + + return traderAccountAfterDeposit; + } + + private AmmInfoResult createAmm( + KeyPair issuerKeyPair, + FeeResult feeResult + ) throws JsonRpcClientErrorException, JsonProcessingException { + AccountInfoResult issuerAccount = scanForResult( + () -> this.getValidatedAccountInfo(issuerKeyPair.publicKey().deriveAddress()) + ); + + enableRippling(issuerKeyPair, issuerAccount, feeResult); + + XrpCurrencyAmount reserveAmount = xrplClient.serverInformation().info() + .map( + rippled -> rippled.closedLedger().orElse(rippled.validatedLedger().get()).reserveIncXrp(), + clio -> clio.validatedLedger().get().reserveIncXrp(), + reporting -> reporting.closedLedger().orElse(reporting.validatedLedger().get()).reserveIncXrp() + ); + AmmCreate ammCreate = AmmCreate.builder() + .account(issuerKeyPair.publicKey().deriveAddress()) + .sequence(issuerAccount.accountData().sequence().plus(UnsignedInteger.ONE)) + .fee(reserveAmount) + .amount( + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .value("25") + .build() + ) + .amount2(XrpCurrencyAmount.ofXrp(BigDecimal.valueOf(100))) + .tradingFee(TradingFee.ofPercent(BigDecimal.ONE)) + .lastLedgerSequence(issuerAccount.ledgerIndexSafe().plus(UnsignedInteger.valueOf(4)).unsignedIntegerValue()) + .signingPublicKey(issuerKeyPair.publicKey()) + .build(); + + SingleSignedTransaction signedCreate = signatureService.sign(issuerKeyPair.privateKey(), ammCreate); + SubmitResult submitResult = xrplClient.submit(signedCreate); + assertThat(submitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + + scanForFinality( + signedCreate.hash(), + issuerAccount.ledgerIndexSafe(), + ammCreate.lastLedgerSequence().get(), + ammCreate.sequence(), + issuerKeyPair.publicKey().deriveAddress() + ); + + return getAmmInfo(issuerKeyPair); + } + + private AmmInfoResult getAmmInfo(KeyPair issuerKeyPair) throws JsonRpcClientErrorException { + AmmInfoResult ammInfoResult = xrplClient.ammInfo( + AmmInfoRequestParams.from( + Issue.XRP, + Issue.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .build() + )); + + AccountInfoResult ammAccountInfo = xrplClient.accountInfo( + AccountInfoRequestParams.of(ammInfoResult.amm().account()) + ); + + assertThat(ammAccountInfo.accountData().ammId()).isNotEmpty(); + + AmmInfoResult ammInfoByAccount = xrplClient.ammInfo( + AmmInfoRequestParams.from(ammAccountInfo.accountData().account()) + ); + + assertThat(ammInfoByAccount).isEqualTo(ammInfoResult); + + LedgerEntryResult ammObject = xrplClient.ledgerEntry( + LedgerEntryRequestParams.amm( + AmmLedgerEntryParams.builder() + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .build() + ) + .build(), + LedgerSpecifier.VALIDATED + ) + ); + + assertThat(ammObject.node().account()).isEqualTo(ammInfoByAccount.amm().account()); + assertThat(ammObject.node().asset()).isEqualTo(Issue.XRP); + assertThat(ammObject.node().asset2()).isEqualTo( + Issue.builder() + .issuer(((IssuedCurrencyAmount) ammInfoByAccount.amm().amount2()).issuer()) + .currency(((IssuedCurrencyAmount) ammInfoByAccount.amm().amount2()).currency()) + .build() + ); + assertThat( + (ammObject.node().auctionSlot().isPresent() && ammInfoByAccount.amm().auctionSlot().isPresent()) || + (!ammObject.node().auctionSlot().isPresent() && !ammInfoByAccount.amm().auctionSlot().isPresent()) + ).isTrue(); + if (ammObject.node().auctionSlot().isPresent()) { + AuctionSlot entryAuctionSlot = ammObject.node().auctionSlot().get(); + AmmInfoAuctionSlot infoAuctionSlot = ammInfoByAccount.amm().auctionSlot().get(); + assertThat(entryAuctionSlot.account()).isEqualTo(infoAuctionSlot.account()); + assertThat(entryAuctionSlot.authAccountsAddresses()).isEqualTo(infoAuctionSlot.authAccounts().stream().map( + AmmInfoAuthAccount::account).collect(Collectors.toList())); + assertThat(entryAuctionSlot.price()).isEqualTo(infoAuctionSlot.price()); + assertThat(TimeUtils.xrplTimeToZonedDateTime(UnsignedLong.valueOf(entryAuctionSlot.expiration().longValue()))) + .isEqualTo(infoAuctionSlot.expiration()); + assertThat(entryAuctionSlot.discountedFee()).isEqualTo(infoAuctionSlot.discountedFee()); + } + + assertThat(ammObject.node().lpTokenBalance()).isEqualTo(ammInfoByAccount.amm().lpToken()); + assertThat(ammObject.node().tradingFee()).isEqualTo(ammInfoByAccount.amm().tradingFee()); + + assertThat(ammObject.node().voteSlots().size()).isEqualTo(ammInfoByAccount.amm().voteSlots().size()); + + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(ammObject.index(), AmmObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(ammObject.node()); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(ammObject.index(), LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); + + return ammInfoResult; + } + + private void enableRippling(KeyPair issuerKeyPair, AccountInfoResult issuerAccount, FeeResult feeResult) + throws JsonRpcClientErrorException, JsonProcessingException { + AccountSet accountSet = AccountSet.builder() + .account(issuerKeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) + .signingPublicKey(issuerKeyPair.publicKey()) + .sequence(issuerAccount.accountData().sequence()) + .setFlag(AccountSetFlag.DEFAULT_RIPPLE) + .build(); + + SingleSignedTransaction signed = signatureService.sign(issuerKeyPair.privateKey(), accountSet); + SubmitResult setResult = xrplClient.submit(signed); + assertThat(setResult.engineResult()).isEqualTo("tesSUCCESS"); + logger.info( + "AccountSet transaction successful: https://testnet.xrpl.org/transactions/{}", + setResult.transactionResult().hash() + ); + + scanForResult( + () -> getValidatedAccountInfo(issuerKeyPair.publicKey().deriveAddress()), + info -> info.accountData().flags().lsfDefaultRipple() + ); + } +} diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/CheckIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/CheckIT.java index 40928ec6d..5e4b93ee2 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/CheckIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/CheckIT.java @@ -30,10 +30,14 @@ import org.xrpl.xrpl4j.crypto.keys.KeyPair; import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; import org.xrpl.xrpl4j.model.ledger.CheckObject; +import org.xrpl.xrpl4j.model.ledger.EscrowObject; import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.transactions.CheckCancel; import org.xrpl.xrpl4j.model.transactions.CheckCash; @@ -97,6 +101,8 @@ public void createXrpCheckAndCash() throws JsonRpcClientErrorException, JsonProc .filter(findCheck(sourceKeyPair, destinationKeyPair, invoiceId)) .findFirst().get(); + assertEntryEqualsObjectFromAccountObjects(checkObject); + ////////////////////// // Destination wallet cashes the Check feeResult = xrplClient.fee(); @@ -188,6 +194,8 @@ public void createCheckAndSourceCancels() throws JsonRpcClientErrorException, Js .filter(findCheck(sourceKeyPair, destinationKeyPair, invoiceId)) .findFirst().get(); + assertEntryEqualsObjectFromAccountObjects(checkObject); + ////////////////////// // Source account cancels the Check feeResult = xrplClient.fee(); @@ -265,6 +273,8 @@ public void createCheckAndDestinationCancels() throws JsonRpcClientErrorExceptio .filter(findCheck(sourceKeyPair, destinationKeyPair, invoiceId)) .findFirst().get(); + assertEntryEqualsObjectFromAccountObjects(checkObject); + ////////////////////// // Destination account cancels the Check feeResult = xrplClient.fee(); @@ -304,4 +314,23 @@ private Predicate findCheck(KeyPair sourceKeyPair, KeyPair destina ((CheckObject) object).destination().equals(destinationKeyPair.publicKey().deriveAddress()); } + + private void assertEntryEqualsObjectFromAccountObjects(CheckObject checkObject) throws JsonRpcClientErrorException { + LedgerEntryResult checkEntry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.check(checkObject.index(), LedgerSpecifier.CURRENT)); + + assertThat(checkEntry.node()).isEqualTo(checkObject); + + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(checkObject.index(), CheckObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(checkEntry.node()); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(checkObject.index(), LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); + } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java new file mode 100644 index 000000000..ace2e32f1 --- /dev/null +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java @@ -0,0 +1,173 @@ +package org.xrpl.xrpl4j.tests; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; +import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; +import org.xrpl.xrpl4j.crypto.keys.KeyPair; +import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; +import org.xrpl.xrpl4j.model.client.accounts.AccountInfoRequestParams; +import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.accounts.TrustLine; +import org.xrpl.xrpl4j.model.client.fees.FeeResult; +import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; +import org.xrpl.xrpl4j.model.transactions.AccountSet; +import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Clawback; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TransactionResultCodes; +import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; + +@DisabledIf(value = "shouldNotRun", disabledReason = "ClawbackIT only runs on local rippled node or devnet.") +public class ClawbackIT extends AbstractIT { + + static boolean shouldNotRun() { + return System.getProperty("useTestnet") != null || + System.getProperty("useClioTestnet") != null; + } + + @Test + void issueBalanceAndClawback() throws JsonRpcClientErrorException, JsonProcessingException { + KeyPair issuerKeyPair = createRandomAccountEd25519(); + KeyPair holderKeyPair = createRandomAccountEd25519(); + + FeeResult feeResult = xrplClient.fee(); + AccountInfoResult issuerAccount = this.scanForResult( + () -> this.getValidatedAccountInfo(issuerKeyPair.publicKey().deriveAddress()) + ); + + XrpCurrencyAmount fee = FeeUtils.computeNetworkFees(feeResult).recommendedFee(); + setAllowClawback(issuerKeyPair, issuerAccount, fee); + + createTrustLine( + "USD", + "10000", + issuerKeyPair, + holderKeyPair, + fee + ); + + sendIssuedCurrency( + "USD", + "100", + issuerKeyPair, + holderKeyPair, + fee + ); + + issuerAccount = this.getValidatedAccountInfo(issuerAccount.accountData().account()); + clawback( + "USD", + "10", + holderKeyPair.publicKey().deriveAddress(), + issuerKeyPair, + issuerAccount, + fee + ); + + TrustLine trustline = this.getValidatedAccountLines( + issuerAccount.accountData().account(), + holderKeyPair.publicKey().deriveAddress() + ).lines().stream() + .filter(line -> line.currency().equals("USD")) + .findFirst() + .orElseThrow(() -> new RuntimeException("No trustline found.")); + + assertThat(trustline.balance()).isEqualTo("-90"); + + issuerAccount = this.getValidatedAccountInfo(issuerAccount.accountData().account()); + clawback( + "USD", + "90", + holderKeyPair.publicKey().deriveAddress(), + issuerKeyPair, + issuerAccount, + fee + ); + + trustline = this.getValidatedAccountLines( + issuerAccount.accountData().account(), + holderKeyPair.publicKey().deriveAddress() + ).lines().stream() + .filter(line -> line.currency().equals("USD")) + .findFirst() + .orElseThrow(() -> new RuntimeException("No trustline found.")); + assertThat(trustline.balance()).isEqualTo("0"); + } + + private void clawback( + String currencyCode, + String amount, + Address holderAddress, + KeyPair issuerKeyPair, + AccountInfoResult issuerAccountInfo, + XrpCurrencyAmount fee + ) throws JsonRpcClientErrorException, JsonProcessingException { + Clawback clawback = Clawback.builder() + .account(issuerKeyPair.publicKey().deriveAddress()) + .fee(fee) + .sequence(issuerAccountInfo.accountData().sequence()) + .signingPublicKey(issuerKeyPair.publicKey()) + .amount( + IssuedCurrencyAmount.builder() + .currency(currencyCode) + .value(amount) + .issuer(holderAddress) + .build() + ) + .lastLedgerSequence(issuerAccountInfo.ledgerIndexSafe().unsignedIntegerValue().plus(UnsignedInteger.valueOf(4))) + .build(); + + SingleSignedTransaction signedClawback = signatureService.sign(issuerKeyPair.privateKey(), clawback); + SubmitResult submitResult = xrplClient.submit(signedClawback); + assertThat(submitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + + scanForFinality( + signedClawback.hash(), + issuerAccountInfo.ledgerIndexSafe(), + clawback.lastLedgerSequence().get(), + clawback.sequence(), + clawback.account() + ); + } + + private void setAllowClawback( + KeyPair issuerKeyPair, + AccountInfoResult issuerAccount, + XrpCurrencyAmount fee + ) throws JsonRpcClientErrorException, JsonProcessingException { + AccountSet accountSet = AccountSet.builder() + .account(issuerAccount.accountData().account()) + .fee(fee) + .sequence(issuerAccount.accountData().sequence()) + .signingPublicKey(issuerKeyPair.publicKey()) + .lastLedgerSequence(issuerAccount.ledgerIndexSafe().unsignedIntegerValue().plus(UnsignedInteger.valueOf(4))) + .setFlag(AccountSetFlag.ALLOW_TRUSTLINE_CLAWBACK) + .build(); + + SingleSignedTransaction signedAccountSet = signatureService.sign( + issuerKeyPair.privateKey(), accountSet + ); + SubmitResult submitResult = xrplClient.submit(signedAccountSet); + assertThat(submitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + + scanForFinality( + signedAccountSet.hash(), + issuerAccount.ledgerIndexSafe(), + accountSet.lastLedgerSequence().get(), + accountSet.sequence(), + accountSet.account() + ); + + AccountInfoResult accountInfoAfterSet = xrplClient.accountInfo( + AccountInfoRequestParams.of(issuerAccount.accountData().account()) + ); + + assertThat(accountInfoAfterSet.accountData().flags().lsfAllowTrustLineClawback()).isTrue(); + } +} diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/DepositPreAuthIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/DepositPreAuthIT.java index 28c86519e..5b9c2d618 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/DepositPreAuthIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/DepositPreAuthIT.java @@ -33,10 +33,16 @@ import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.DepositPreAuthLedgerEntryParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedRequestParams; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; +import org.xrpl.xrpl4j.model.ledger.CheckObject; import org.xrpl.xrpl4j.model.ledger.DepositPreAuthObject; +import org.xrpl.xrpl4j.model.ledger.EscrowObject; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.transactions.AccountSet; import org.xrpl.xrpl4j.model.transactions.DepositPreAuth; import org.xrpl.xrpl4j.model.transactions.Hash256; @@ -85,14 +91,20 @@ public void preauthorizeAccountAndReceivePayment() throws JsonRpcClientErrorExce ///////////////////////// // Validate that the DepositPreAuthObject was added to the receiver's account objects - this.scanForResult( + DepositPreAuthObject preAuthObject = (DepositPreAuthObject) this.scanForResult( () -> this.getValidatedAccountObjects(receiverKeyPair.publicKey().deriveAddress()), accountObjects -> accountObjects.accountObjects().stream().anyMatch(object -> DepositPreAuthObject.class.isAssignableFrom(object.getClass()) && ((DepositPreAuthObject) object).authorize().equals(senderKeyPair.publicKey().deriveAddress()) ) - ); + ).accountObjects().stream() + .filter(object -> DepositPreAuthObject.class.isAssignableFrom(object.getClass()) && + ((DepositPreAuthObject) object).authorize().equals(senderKeyPair.publicKey().deriveAddress())) + .findFirst() + .get(); + + assertEntryEqualsObjectFromAccountObjects(depositPreAuth, preAuthObject); ///////////////////////// // Validate that the `deposit_authorized` client call is implemented properly by ensuring it aligns with the @@ -266,4 +278,34 @@ private AccountInfoResult enableDepositPreauth( accountInfo -> accountInfo.accountData().flags().lsfDepositAuth() ); } + + private void assertEntryEqualsObjectFromAccountObjects( + DepositPreAuth depositPreAuth, + DepositPreAuthObject preAuthObject + ) + throws JsonRpcClientErrorException { + LedgerEntryResult preAuthEntry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.depositPreAuth( + DepositPreAuthLedgerEntryParams.builder() + .owner(depositPreAuth.account()) + .authorized(depositPreAuth.authorize().get()) + .build(), + LedgerSpecifier.CURRENT + ) + ); + + assertThat(preAuthEntry.node()).isEqualTo(preAuthObject); + + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(preAuthObject.index(), DepositPreAuthObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(preAuthEntry.node()); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(preAuthObject.index(), LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); + } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/EscrowIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/EscrowIT.java index fed3ba8a9..5d34919b7 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/EscrowIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/EscrowIT.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. @@ -27,16 +27,25 @@ import com.google.common.primitives.UnsignedLong; import com.ripple.cryptoconditions.PreimageSha256Fulfillment; import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.com.github.dockerjava.core.dockerfile.DockerfileStatement.Add; import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; import org.xrpl.xrpl4j.crypto.keys.KeyPair; import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsRequestParams; +import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsRequestParams.AccountObjectType; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; +import org.xrpl.xrpl4j.model.client.ledger.EscrowLedgerEntryParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; import org.xrpl.xrpl4j.model.client.ledger.LedgerResult; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; import org.xrpl.xrpl4j.model.immutables.FluentCompareTo; import org.xrpl.xrpl4j.model.ledger.EscrowObject; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; +import org.xrpl.xrpl4j.model.transactions.Address; import org.xrpl.xrpl4j.model.transactions.EscrowCancel; import org.xrpl.xrpl4j.model.transactions.EscrowCreate; import org.xrpl.xrpl4j.model.transactions.EscrowFinish; @@ -92,6 +101,11 @@ public void createAndFinishTimeBasedEscrow() throws JsonRpcClientErrorException, () -> this.getValidatedTransaction(createResult.transactionResult().hash(), EscrowCreate.class) ); + assertEntryEqualsObjectFromAccountObjects( + senderKeyPair.publicKey().deriveAddress(), + escrowCreate.sequence() + ); + ////////////////////// // Wait until the close time on the current validated ledger is after the finishAfter time on the Escrow this.scanForResult( @@ -188,7 +202,7 @@ public void createAndCancelTimeBasedEscrow() throws JsonRpcClientErrorException, ////////////////////// // Then wait until the transaction gets committed to a validated ledger - TransactionResult result = this.scanForResult( + final TransactionResult result = this.scanForResult( () -> this.getValidatedTransaction(createResult.transactionResult().hash(), EscrowCreate.class) ); @@ -201,6 +215,11 @@ public void createAndCancelTimeBasedEscrow() throws JsonRpcClientErrorException, ) ); + assertEntryEqualsObjectFromAccountObjects( + senderKeyPair.publicKey().deriveAddress(), + escrowCreate.sequence() + ); + ////////////////////// // Wait until the close time on the current validated ledger is after the cancelAfter time on the Escrow this.scanForResult( @@ -298,6 +317,11 @@ public void createAndFinishCryptoConditionBasedEscrow() throws JsonRpcClientErro () -> this.getValidatedTransaction(createResult.transactionResult().hash(), EscrowCreate.class) ); + assertEntryEqualsObjectFromAccountObjects( + senderKeyPair.publicKey().deriveAddress(), + escrowCreate.sequence() + ); + ////////////////////// // Wait until the close time on the current validated ledger is after the finishAfter time on the Escrow this.scanForResult( @@ -405,6 +429,11 @@ public void createAndCancelCryptoConditionBasedEscrow() throws JsonRpcClientErro () -> this.getValidatedTransaction(createResult.transactionResult().hash(), EscrowCreate.class) ); + assertEntryEqualsObjectFromAccountObjects( + senderKeyPair.publicKey().deriveAddress(), + escrowCreate.sequence() + ); + ////////////////////// // Wait until the close time on the current validated ledger is after the cancelAfter time on the Escrow this.scanForResult( @@ -475,4 +504,41 @@ private Instant getMinExpirationTime() { return closeTime.isBefore(now) ? now : closeTime; } + + private void assertEntryEqualsObjectFromAccountObjects( + Address escrowOwner, + UnsignedInteger createSequence + ) throws JsonRpcClientErrorException { + + EscrowObject escrowObject = (EscrowObject) xrplClient.accountObjects(AccountObjectsRequestParams.builder() + .type(AccountObjectType.ESCROW) + .account(escrowOwner) + .ledgerSpecifier(LedgerSpecifier.VALIDATED) + .build() + ).accountObjects().get(0); + + LedgerEntryResult escrowEntry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.escrow( + EscrowLedgerEntryParams.builder() + .owner(escrowOwner) + .seq(createSequence) + .build(), + LedgerSpecifier.VALIDATED + ) + ); + + assertThat(escrowEntry.node()).isEqualTo(escrowObject); + + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(escrowObject.index(), EscrowObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(escrowEntry.node()); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(escrowObject.index(), LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); + } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java index 5ab7f776e..670135a9a 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.base.Strings; import com.google.common.io.BaseEncoding; +import com.google.common.primitives.UnsignedInteger; import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; import org.xrpl.xrpl4j.crypto.keys.KeyPair; @@ -32,12 +33,21 @@ import org.xrpl.xrpl4j.model.client.accounts.AccountCurrenciesRequestParams; import org.xrpl.xrpl4j.model.client.accounts.AccountCurrenciesResult; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsRequestParams; import org.xrpl.xrpl4j.model.client.accounts.TrustLine; import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; +import org.xrpl.xrpl4j.model.client.ledger.RippleStateLedgerEntryParams; +import org.xrpl.xrpl4j.model.client.ledger.RippleStateLedgerEntryParams.RippleStateAccounts; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; +import org.xrpl.xrpl4j.model.ledger.EscrowObject; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; +import org.xrpl.xrpl4j.model.ledger.RippleStateObject; import org.xrpl.xrpl4j.model.transactions.AccountSet; +import org.xrpl.xrpl4j.model.transactions.Address; import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; import org.xrpl.xrpl4j.model.transactions.PathStep; import org.xrpl.xrpl4j.model.transactions.Payment; @@ -77,6 +87,12 @@ void createTrustlineWithMaxLimit() throws JsonRpcClientErrorException, JsonProce FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); + assertThatEntryEqualsObjectFromAccountObjects( + trustLine, + issuerKeyPair.publicKey().deriveAddress(), + xrpl4jCoin + ); + assertThat(trustLine.limitPeer()).isEqualTo("9999999999999999e80"); } @@ -102,6 +118,12 @@ void createTrustlineWithMaxLimitMinusOneExponent() throws JsonRpcClientErrorExce FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); + assertThatEntryEqualsObjectFromAccountObjects( + trustLine, + issuerKeyPair.publicKey().deriveAddress(), + xrpl4jCoin + ); + assertThat(trustLine.limitPeer()).isEqualTo("9999999999999999e79"); } @@ -127,6 +149,12 @@ void createTrustlineWithSmallestPositiveLimit() throws JsonRpcClientErrorExcepti FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); + assertThatEntryEqualsObjectFromAccountObjects( + trustLine, + issuerKeyPair.publicKey().deriveAddress(), + xrpl4jCoin + ); + assertThat(trustLine.limitPeer()).isEqualTo("1000000000000000e-96"); } @@ -154,6 +182,12 @@ void createTrustlineWithSmalletPositiveLimitPlusOne() throws JsonRpcClientErrorE FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); + assertThatEntryEqualsObjectFromAccountObjects( + trustLine, + issuerKeyPair.publicKey().deriveAddress(), + xrpl4jCoin + ); + assertThat(trustLine.limitPeer()).isEqualTo("1100000000000000e-96"); } @@ -179,9 +213,15 @@ public void issueIssuedCurrencyBalance() throws JsonRpcClientErrorException, Jso FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); + assertThatEntryEqualsObjectFromAccountObjects( + trustLine, + issuerKeyPair.publicKey().deriveAddress(), + xrpl4jCoin + ); + /////////////////////////// // Send some xrpl4jCoin to the counterparty account. - issueBalance( + sendIssuedCurrency( xrpl4jCoin, trustLine.limitPeer(), issuerKeyPair, counterpartyKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -189,9 +229,19 @@ public void issueIssuedCurrencyBalance() throws JsonRpcClientErrorException, Jso /////////////////////////// // Validate that the TrustLine balance was updated as a result of the Payment. // The trust line returned is from the perspective of the issuer, so the balance should be negative. - this.scanForResult(() -> getValidatedAccountLines(issuerKeyPair.publicKey().deriveAddress(), - counterpartyKeyPair.publicKey().deriveAddress()), + TrustLine trustLineAfterPayment = this.scanForResult( + () -> getValidatedAccountLines(issuerKeyPair.publicKey().deriveAddress(), + counterpartyKeyPair.publicKey().deriveAddress()), linesResult -> linesResult.lines().stream().anyMatch(line -> line.balance().equals("-" + trustLine.limitPeer())) + ).lines().stream() + .filter(line -> line.balance().equals("-" + trustLine.limitPeer())) + .findFirst() + .get(); + + assertThatEntryEqualsObjectFromAccountObjects( + trustLineAfterPayment, + issuerKeyPair.publicKey().deriveAddress(), + xrpl4jCoin ); /////////////////////////// @@ -245,11 +295,23 @@ public void sendSimpleRipplingIssuedCurrencyPayment() throws JsonRpcClientErrorE /////////////////////////// // Issuer issues 50 USD to alice - issueBalance("USD", "50", issuerKeyPair, aliceKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "50", + issuerKeyPair, + aliceKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Issuer issues 50 USD to bob - issueBalance("USD", "50", issuerKeyPair, bobKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "50", + issuerKeyPair, + bobKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Try to find a path for this Payment. @@ -386,22 +448,46 @@ public void sendMultiHopSameCurrencyPayment() throws JsonRpcClientErrorException /////////////////////////// // Issue 10 USD from issuerA to charlie. // IssuerA now owes Charlie 10 USD. - issueBalance("USD", "10", issuerAKeyPair, charlieKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "10", + issuerAKeyPair, + charlieKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Issue 1 USD from issuerA to emily. // IssuerA now owes Emily 1 USD - issueBalance("USD", "1", issuerAKeyPair, emilyKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "1", + issuerAKeyPair, + emilyKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Issue 100 USD from issuerB to emily. // IssuerB now owes Emily 100 USD - issueBalance("USD", "100", issuerBKeyPair, emilyKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "100", + issuerBKeyPair, + emilyKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Issue 2 USD from issuerB to daniel. // IssuerB now owes Daniel 2 USD - issueBalance("USD", "2", issuerBKeyPair, danielKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "2", + issuerBKeyPair, + danielKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Look for a payment path from charlie to daniel. @@ -523,106 +609,42 @@ public void setDefaultRipple(KeyPair issuerKeyPair, FeeResult feeResult) ); } - /** - * Send issued currency funds from an issuer to a counterparty. - * - * @param currency The currency code to send. - * @param value The amount of currency to send. - * @param issuerKeyPair The {@link KeyPair} of the issuer account. - * @param counterpartyKeyPair The {@link KeyPair} of the counterparty account. - * @param fee The current network fee, as an {@link XrpCurrencyAmount}. - * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. - */ - public void issueBalance( - String currency, - String value, - KeyPair issuerKeyPair, - KeyPair counterpartyKeyPair, - XrpCurrencyAmount fee - ) throws JsonRpcClientErrorException, JsonProcessingException { - /////////////////////////// - // Issuer sends a payment with the issued currency to the counterparty - AccountInfoResult issuerAccountInfo = this.scanForResult( - () -> getValidatedAccountInfo(issuerKeyPair.publicKey().deriveAddress()) + + private void assertThatEntryEqualsObjectFromAccountObjects( + TrustLine trustLine, + Address peerAddress, + String currency + ) throws JsonRpcClientErrorException { + RippleStateObject rippleStateObject = (RippleStateObject) xrplClient.accountObjects( + AccountObjectsRequestParams.of(trustLine.account()) + ).accountObjects().stream() + .filter(object -> RippleStateObject.class.isAssignableFrom(object.getClass()) /*&&*/ + /*((RippleStateObject) object).previousTransactionLedgerSequence().equals(lastSequence)*/) + .findFirst() + .get(); + + LedgerEntryResult entry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.rippleState( + RippleStateLedgerEntryParams.builder() + .accounts(RippleStateAccounts.of(trustLine.account(), peerAddress)) + .currency(currency) + .build(), + LedgerSpecifier.VALIDATED + ) ); - Payment fundCounterparty = Payment.builder() - .account(issuerKeyPair.publicKey().deriveAddress()) - .fee(fee) - .sequence(issuerAccountInfo.accountData().sequence()) - .destination(counterpartyKeyPair.publicKey().deriveAddress()) - .amount(IssuedCurrencyAmount.builder() - .issuer(issuerKeyPair.publicKey().deriveAddress()) - .currency(currency) - .value(value) - .build()) - .signingPublicKey(issuerKeyPair.publicKey()) - .build(); + assertThat(entry.node()).isEqualTo(rippleStateObject); - SingleSignedTransaction signedFundCounterparty = signatureService.sign( - issuerKeyPair.privateKey(), fundCounterparty - ); - SubmitResult paymentResult = xrplClient.submit(signedFundCounterparty); - assertThat(paymentResult.engineResult()).isEqualTo("tesSUCCESS"); - logger.info( - "Payment transaction successful: https://testnet.xrpl.org/transactions/{}", - paymentResult.transactionResult().hash() + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(rippleStateObject.index(), RippleStateObject.class, LedgerSpecifier.VALIDATED) ); - this.scanForResult(() -> getValidatedTransaction(paymentResult.transactionResult().hash(), Payment.class)); - } - - /** - * Create a trustline between the given issuer and counterparty accounts for the given currency code and with the - * given limit. - * - * @param currency The currency code of the trustline to create. - * @param value The trustline limit of the trustline to create. - * @param issuerKeyPair The {@link KeyPair} of the issuer account. - * @param counterpartyKeyPair The {@link KeyPair} of the counterparty account. - * @param fee The current network fee, as an {@link XrpCurrencyAmount}. - * @return The {@link TrustLine} that gets created. - * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. - */ - public TrustLine createTrustLine( - String currency, - String value, - KeyPair issuerKeyPair, - KeyPair counterpartyKeyPair, - XrpCurrencyAmount fee - ) throws JsonRpcClientErrorException, JsonProcessingException { - AccountInfoResult counterpartyAccountInfo = this.scanForResult( - () -> this.getValidatedAccountInfo(counterpartyKeyPair.publicKey().deriveAddress()) - ); - - TrustSet trustSet = TrustSet.builder() - .account(counterpartyKeyPair.publicKey().deriveAddress()) - .fee(fee) - .sequence(counterpartyAccountInfo.accountData().sequence()) - .limitAmount(IssuedCurrencyAmount.builder() - .currency(currency) - .issuer(issuerKeyPair.publicKey().deriveAddress()) - .value(value) - .build()) - .signingPublicKey(counterpartyKeyPair.publicKey()) - .build(); + assertThat(entryByIndex.node()).isEqualTo(entry.node()); - SingleSignedTransaction signedTrustSet = signatureService.sign(counterpartyKeyPair.privateKey(), - trustSet); - SubmitResult trustSetSubmitResult = xrplClient.submit(signedTrustSet); - assertThat(trustSetSubmitResult.engineResult()).isEqualTo("tesSUCCESS"); - logger.info( - "TrustSet transaction successful: https://testnet.xrpl.org/transactions/{}", - trustSetSubmitResult.transactionResult().hash() + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(rippleStateObject.index(), LedgerSpecifier.VALIDATED) ); - return scanForResult( - () -> - getValidatedAccountLines(issuerKeyPair.publicKey().deriveAddress(), - counterpartyKeyPair.publicKey().deriveAddress()), - linesResult -> !linesResult.lines().isEmpty() - ) - .lines().get(0); + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); } - } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NfTokenIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NfTokenIT.java index cc0a143d2..5204f08f8 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NfTokenIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NfTokenIT.java @@ -26,6 +26,7 @@ import com.google.common.collect.Lists; import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; import org.xrpl.xrpl4j.crypto.keys.KeyPair; @@ -34,9 +35,11 @@ import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; import org.xrpl.xrpl4j.model.client.accounts.AccountNftsResult; import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsRequestParams; -import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsResult; import org.xrpl.xrpl4j.model.client.accounts.NfTokenObject; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersRequestParams; import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersResult; import org.xrpl.xrpl4j.model.client.nft.NftSellOffersRequestParams; @@ -112,7 +115,7 @@ void mint() throws JsonRpcClientErrorException, JsonProcessingException { }, result -> result.accountNfts().stream() .anyMatch(nft -> nft.uri().get().equals(uri)) - ) + ) .accountNfts() .stream().filter(nft -> nft.uri().get().equals(uri)) .findFirst() @@ -121,22 +124,7 @@ void mint() throws JsonRpcClientErrorException, JsonProcessingException { assertThat(validatedMint.metadata().flatMap(TransactionMetadata::nfTokenId)) .isNotEmpty().get().isEqualTo(nfToken.nfTokenId()); - Optional maybeNfTokenPage = xrplClient.accountObjects( - AccountObjectsRequestParams.of(nfTokenMint.account()) - ).accountObjects().stream() - .filter(object -> NfTokenPageObject.class.isAssignableFrom(object.getClass())) - .map(object -> (NfTokenPageObject) object) - .findFirst(); - - assertThat(maybeNfTokenPage).isNotEmpty(); - assertThat(maybeNfTokenPage.get().nfTokens()).contains( - NfTokenWrapper.of( - NfToken.builder() - .nfTokenId(nfToken.nfTokenId()) - .uri(nfToken.uri()) - .build() - ) - ); + assertEntryEqualsObjectFromAccountObjects(keyPair.publicKey().deriveAddress(), nfToken); AccountInfoResult minterAccountInfo = xrplClient.accountInfo( AccountInfoRequestParams.of(keyPair.publicKey().deriveAddress()) @@ -198,18 +186,9 @@ void mintFromOtherMinterAccount() throws JsonRpcClientErrorException, JsonProces assertThat(mintSubmitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); assertThat(signedMint.hash()).isEqualTo(mintSubmitResult.transactionResult().hash()); - this.scanForResult( - () -> { - try { - return xrplClient.accountNfts(minterKeyPair.publicKey().deriveAddress()); - } catch (JsonRpcClientErrorException e) { - logger.error("Exception occurred while getting account nfts: {}", e.getMessage(), e); - throw new RuntimeException(e); - } - }, - result -> result.accountNfts().stream() - .anyMatch(nft -> nft.uri().get().equals(uri)) - ); + NfTokenObject nfToken = scanForNfToken(minterKeyPair, uri); + + assertEntryEqualsObjectFromAccountObjects(minterKeyPair.publicKey().deriveAddress(), nfToken); AccountInfoResult sourceAccountInfoAfterMint = xrplClient.accountInfo( AccountInfoRequestParams.of(keyPair.publicKey().deriveAddress()) @@ -247,19 +226,11 @@ void mintAndBurn() throws JsonRpcClientErrorException, JsonProcessingException { assertThat(mintSubmitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); assertThat(signedMint.hash()).isEqualTo(mintSubmitResult.transactionResult().hash()); - this.scanForResult( - () -> { - try { - return xrplClient.accountNfts(keyPair.publicKey().deriveAddress()); - } catch (JsonRpcClientErrorException e) { - throw new RuntimeException(e); - } - }, - result -> result.accountNfts().stream() - .anyMatch(nft -> nft.uri().get().equals(uri)) - ); + NfTokenObject nfToken = scanForNfToken(keyPair, uri); logger.info("NFT was minted successfully."); + assertEntryEqualsObjectFromAccountObjects(keyPair.publicKey().deriveAddress(), nfToken); + // nft burn AccountNftsResult accountNftsResult = xrplClient.accountNfts(keyPair.publicKey().deriveAddress()); @@ -335,19 +306,11 @@ void mintAndCreateOffer() throws JsonRpcClientErrorException, JsonProcessingExce mintSubmitResult.transactionResult().hash() ); - this.scanForResult( - () -> { - try { - return xrplClient.accountNfts(keyPair.publicKey().deriveAddress()); - } catch (JsonRpcClientErrorException e) { - throw new RuntimeException(e); - } - }, - result -> result.accountNfts().stream() - .anyMatch(nft -> nft.uri().get().equals(uri)) - ); + NfTokenObject nfToken = scanForNfToken(keyPair, uri); logger.info("NFT was minted successfully."); + assertEntryEqualsObjectFromAccountObjects(keyPair.publicKey().deriveAddress(), nfToken); + //create a sell offer for the NFT that was created above NfTokenId tokenId = xrplClient.accountNfts(keyPair.publicKey().deriveAddress()).accountNfts().get(0).nfTokenId(); @@ -394,6 +357,12 @@ void mintAndCreateOffer() throws JsonRpcClientErrorException, JsonProcessingExce .findFirst() .get(); + LedgerEntryResult entry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(nfTokenOffer.index(), NfTokenOfferObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entry.node()).isEqualTo(nfTokenOffer); + assertThat(validatedOfferCreate.metadata().flatMap(TransactionMetadata::offerId)).isNotEmpty().get() .isEqualTo(nfTokenOffer.index()); logger.info("NFTokenOffer object was found in account's objects."); @@ -401,20 +370,20 @@ void mintAndCreateOffer() throws JsonRpcClientErrorException, JsonProcessingExce @Test void mintAndCreateThenAcceptOffer() throws JsonRpcClientErrorException, JsonProcessingException { - KeyPair wallet = createRandomAccountEd25519(); + KeyPair keypair = createRandomAccountEd25519(); //mint NFT from one account AccountInfoResult accountInfoResult = this.scanForResult( - () -> this.getValidatedAccountInfo(wallet.publicKey().deriveAddress()) + () -> this.getValidatedAccountInfo(keypair.publicKey().deriveAddress()) ); NfTokenUri uri = NfTokenUri.ofPlainText("ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf4dfuylqabf3oclgtqy55fbzdi"); //Nft mint transaction NfTokenMint nfTokenMint = NfTokenMint.builder() .tokenTaxon(UnsignedLong.ONE) - .account(wallet.publicKey().deriveAddress()) + .account(keypair.publicKey().deriveAddress()) .fee(XrpCurrencyAmount.ofDrops(50)) - .signingPublicKey(wallet.publicKey()) + .signingPublicKey(keypair.publicKey()) .sequence(accountInfoResult.accountData().sequence()) .flags(NfTokenMintFlags.builder() .tfTransferable(true) @@ -422,27 +391,19 @@ void mintAndCreateThenAcceptOffer() throws JsonRpcClientErrorException, JsonProc .uri(uri) .build(); - SingleSignedTransaction signedMint = signatureService.sign(wallet.privateKey(), nfTokenMint); + SingleSignedTransaction signedMint = signatureService.sign(keypair.privateKey(), nfTokenMint); SubmitResult mintSubmitResult = xrplClient.submit(signedMint); assertThat(mintSubmitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); assertThat(signedMint.hash()).isEqualTo(mintSubmitResult.transactionResult().hash()); - this.scanForResult( - () -> { - try { - return xrplClient.accountNfts(wallet.publicKey().deriveAddress()); - } catch (JsonRpcClientErrorException e) { - throw new RuntimeException(e); - } - }, - result -> result.accountNfts().stream() - .anyMatch(nft -> nft.uri().get().equals(uri)) - ); + NfTokenObject nfToken = scanForNfToken(keypair, uri); logger.info("NFT was minted successfully."); + assertEntryEqualsObjectFromAccountObjects(keypair.publicKey().deriveAddress(), nfToken); + //create a sell offer for the NFT that was created above - NfTokenId tokenId = xrplClient.accountNfts(wallet.publicKey().deriveAddress()).accountNfts().get(0).nfTokenId(); + NfTokenId tokenId = xrplClient.accountNfts(keypair.publicKey().deriveAddress()).accountNfts().get(0).nfTokenId(); // create buy offer from another account KeyPair wallet2 = createRandomAccountEd25519(); @@ -453,7 +414,7 @@ void mintAndCreateThenAcceptOffer() throws JsonRpcClientErrorException, JsonProc NfTokenCreateOffer nfTokenCreateOffer = NfTokenCreateOffer.builder() .account(wallet2.publicKey().deriveAddress()) - .owner(wallet.publicKey().deriveAddress()) + .owner(keypair.publicKey().deriveAddress()) .nfTokenId(tokenId) .fee(XrpCurrencyAmount.ofDrops(50)) .sequence(accountInfoResult2.accountData().sequence()) @@ -478,14 +439,26 @@ void mintAndCreateThenAcceptOffer() throws JsonRpcClientErrorException, JsonProc ); logger.info("NFT Create Offer (Buy) transaction was validated successfully."); - this.scanForResult( + NfTokenOfferObject nfTokenOffer = (NfTokenOfferObject) this.scanForResult( () -> this.getValidatedAccountObjects(wallet2.publicKey().deriveAddress()), objectsResult -> objectsResult.accountObjects().stream() .anyMatch(object -> NfTokenOfferObject.class.isAssignableFrom(object.getClass()) && ((NfTokenOfferObject) object).owner().equals(wallet2.publicKey().deriveAddress()) ) + ).accountObjects() + .stream() + .filter(object -> NfTokenOfferObject.class.isAssignableFrom(object.getClass()) && + ((NfTokenOfferObject) object).owner().equals(wallet2.publicKey().deriveAddress())) + .findFirst() + .get(); + + LedgerEntryResult entry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(nfTokenOffer.index(), NfTokenOfferObject.class, LedgerSpecifier.VALIDATED) ); + + assertThat(entry.node()).isEqualTo(nfTokenOffer); + logger.info("NFTokenOffer object was found in account's objects."); NftBuyOffersResult nftBuyOffersResult = xrplClient.nftBuyOffers(NftBuyOffersRequestParams.builder() @@ -495,15 +468,15 @@ void mintAndCreateThenAcceptOffer() throws JsonRpcClientErrorException, JsonProc // accept offer from a different account NfTokenAcceptOffer nfTokenAcceptOffer = NfTokenAcceptOffer.builder() - .account(wallet.publicKey().deriveAddress()) + .account(keypair.publicKey().deriveAddress()) .buyOffer(nftBuyOffersResult.offers().get(0).nftOfferIndex()) .fee(XrpCurrencyAmount.ofDrops(50)) .sequence(accountInfoResult.accountData().sequence().plus(UnsignedInteger.ONE)) - .signingPublicKey(wallet.publicKey()) + .signingPublicKey(keypair.publicKey()) .build(); SingleSignedTransaction signedAccept = signatureService.sign( - wallet.privateKey(), + keypair.privateKey(), nfTokenAcceptOffer ); SubmitResult nfTokenAcceptOfferSubmitResult = xrplClient.submit(signedAccept); @@ -518,7 +491,7 @@ void mintAndCreateThenAcceptOffer() throws JsonRpcClientErrorException, JsonProc ); logger.info("NFT Accept Offer transaction was validated successfully."); - assertThat(xrplClient.accountNfts(wallet.publicKey().deriveAddress()).accountNfts().size()).isEqualTo(0); + assertThat(xrplClient.accountNfts(keypair.publicKey().deriveAddress()).accountNfts().size()).isEqualTo(0); assertThat(xrplClient.accountNfts(wallet2.publicKey().deriveAddress()).accountNfts().size()).isEqualTo(1); logger.info("The NFT ownership was transferred."); @@ -526,58 +499,49 @@ void mintAndCreateThenAcceptOffer() throws JsonRpcClientErrorException, JsonProc @Test void mintAndCreateOfferThenCancelOffer() throws JsonRpcClientErrorException, JsonProcessingException { - KeyPair wallet = createRandomAccountEd25519(); + KeyPair keypair = createRandomAccountEd25519(); //mint NFT from one account AccountInfoResult accountInfoResult = this.scanForResult( - () -> this.getValidatedAccountInfo(wallet.publicKey().deriveAddress()) + () -> this.getValidatedAccountInfo(keypair.publicKey().deriveAddress()) ); NfTokenUri uri = NfTokenUri.ofPlainText("ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf4dfuylqabf3oclgtqy55fbzdi"); //Nft mint transaction NfTokenMint nfTokenMint = NfTokenMint.builder() .tokenTaxon(UnsignedLong.ONE) - .account(wallet.publicKey().deriveAddress()) + .account(keypair.publicKey().deriveAddress()) .fee(XrpCurrencyAmount.ofDrops(50)) - .signingPublicKey(wallet.publicKey()) + .signingPublicKey(keypair.publicKey()) .sequence(accountInfoResult.accountData().sequence()) .uri(uri) .build(); - SingleSignedTransaction signedMint = signatureService.sign(wallet.privateKey(), nfTokenMint); + SingleSignedTransaction signedMint = signatureService.sign(keypair.privateKey(), nfTokenMint); SubmitResult mintSubmitResult = xrplClient.submit(signedMint); assertThat(mintSubmitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); assertThat(signedMint.hash()).isEqualTo(mintSubmitResult.transactionResult().hash()); - this.scanForResult( - () -> { - try { - return xrplClient.accountNfts(wallet.publicKey().deriveAddress()); - } catch (JsonRpcClientErrorException e) { - throw new RuntimeException(e); - } - }, - result -> result.accountNfts().stream() - .anyMatch(nft -> nft.uri().get().equals(uri)) - ); + scanForNfToken(keypair, uri); + logger.info("NFT was minted successfully."); //create a sell offer for the NFT that was created above - NfTokenId tokenId = xrplClient.accountNfts(wallet.publicKey().deriveAddress()).accountNfts().get(0).nfTokenId(); + NfTokenId tokenId = xrplClient.accountNfts(keypair.publicKey().deriveAddress()).accountNfts().get(0).nfTokenId(); NfTokenCreateOffer nfTokenCreateOffer = NfTokenCreateOffer.builder() - .account(wallet.publicKey().deriveAddress()) + .account(keypair.publicKey().deriveAddress()) .nfTokenId(tokenId) .fee(XrpCurrencyAmount.ofDrops(50)) .sequence(accountInfoResult.accountData().sequence().plus(UnsignedInteger.ONE)) .amount(XrpCurrencyAmount.ofDrops(1000)) .flags(NfTokenCreateOfferFlags.SELL_NFTOKEN) - .signingPublicKey(wallet.publicKey()) + .signingPublicKey(keypair.publicKey()) .build(); SingleSignedTransaction signedOffer = signatureService.sign( - wallet.privateKey(), + keypair.privateKey(), nfTokenCreateOffer ); SubmitResult nfTokenCreateOfferSubmitResult = xrplClient.submit(signedOffer); @@ -594,11 +558,11 @@ void mintAndCreateOfferThenCancelOffer() throws JsonRpcClientErrorException, Jso logger.info("NFT Create Offer (Sell) transaction was validated successfully."); this.scanForResult( - () -> this.getValidatedAccountObjects(wallet.publicKey().deriveAddress()), + () -> this.getValidatedAccountObjects(keypair.publicKey().deriveAddress()), objectsResult -> objectsResult.accountObjects().stream() .anyMatch(object -> NfTokenOfferObject.class.isAssignableFrom(object.getClass()) && - ((NfTokenOfferObject) object).owner().equals(wallet.publicKey().deriveAddress()) + ((NfTokenOfferObject) object).owner().equals(keypair.publicKey().deriveAddress()) ) ); logger.info("NFTokenOffer object was found in account's objects."); @@ -610,14 +574,14 @@ void mintAndCreateOfferThenCancelOffer() throws JsonRpcClientErrorException, Jso // cancel the created offer NfTokenCancelOffer nfTokenCancelOffer = NfTokenCancelOffer.builder() .addTokenOffers(nftSellOffersResult.offers().get(0).nftOfferIndex()) - .account(wallet.publicKey().deriveAddress()) + .account(keypair.publicKey().deriveAddress()) .fee(XrpCurrencyAmount.ofDrops(50)) .sequence(accountInfoResult.accountData().sequence().plus(UnsignedInteger.valueOf(2))) - .signingPublicKey(wallet.publicKey()) + .signingPublicKey(keypair.publicKey()) .build(); SingleSignedTransaction signedCancel = signatureService.sign( - wallet.privateKey(), + keypair.privateKey(), nfTokenCancelOffer ); SubmitResult nfTokenCancelOfferSubmitResult = xrplClient.submit(signedCancel); @@ -637,7 +601,7 @@ void mintAndCreateOfferThenCancelOffer() throws JsonRpcClientErrorException, Jso .isEqualTo(Lists.newArrayList(tokenId)); this.scanForResult( - () -> this.getValidatedAccountObjects(wallet.publicKey().deriveAddress()), + () -> this.getValidatedAccountObjects(keypair.publicKey().deriveAddress()), objectsResult -> objectsResult.accountObjects().stream() .noneMatch(object -> NfTokenOfferObject.class.isAssignableFrom(object.getClass()) @@ -648,20 +612,20 @@ void mintAndCreateOfferThenCancelOffer() throws JsonRpcClientErrorException, Jso @Test void acceptOfferDirectModeWithBrokerFee() throws JsonRpcClientErrorException, JsonProcessingException { - KeyPair wallet = createRandomAccountEd25519(); + KeyPair keypair = createRandomAccountEd25519(); //mint NFT from one account AccountInfoResult accountInfoResult = this.scanForResult( - () -> this.getValidatedAccountInfo(wallet.publicKey().deriveAddress()) + () -> this.getValidatedAccountInfo(keypair.publicKey().deriveAddress()) ); NfTokenUri uri = NfTokenUri.ofPlainText("ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf4dfuylqabf3oclgtqy55fbzdi"); //Nft mint transaction NfTokenMint nfTokenMint = NfTokenMint.builder() .tokenTaxon(UnsignedLong.ONE) - .account(wallet.publicKey().deriveAddress()) + .account(keypair.publicKey().deriveAddress()) .fee(XrpCurrencyAmount.ofDrops(50)) - .signingPublicKey(wallet.publicKey()) + .signingPublicKey(keypair.publicKey()) .sequence(accountInfoResult.accountData().sequence()) .flags(NfTokenMintFlags.builder() .tfTransferable(true) @@ -669,41 +633,32 @@ void acceptOfferDirectModeWithBrokerFee() throws JsonRpcClientErrorException, Js .uri(uri) .build(); - SingleSignedTransaction signedMint = signatureService.sign(wallet.privateKey(), nfTokenMint); + SingleSignedTransaction signedMint = signatureService.sign(keypair.privateKey(), nfTokenMint); SubmitResult mintSubmitResult = xrplClient.submit(signedMint); assertThat(mintSubmitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); assertThat(signedMint.hash()).isEqualTo(mintSubmitResult.transactionResult().hash()); - this.scanForResult( - () -> { - try { - return xrplClient.accountNfts(wallet.publicKey().deriveAddress()); - } catch (JsonRpcClientErrorException e) { - throw new RuntimeException(e); - } - }, - result -> result.accountNfts().stream() - .anyMatch(nft -> nft.uri().get().equals(uri)) - ); + scanForNfToken(keypair, uri); + logger.info("NFT was minted successfully."); //create a sell offer for the NFT that was created above - NfTokenId tokenId = xrplClient.accountNfts(wallet.publicKey().deriveAddress()).accountNfts().get(0).nfTokenId(); + NfTokenId tokenId = xrplClient.accountNfts(keypair.publicKey().deriveAddress()).accountNfts().get(0).nfTokenId(); // the owner creates the sell offer NfTokenCreateOffer nfTokenCreateSellOffer = NfTokenCreateOffer.builder() - .account(wallet.publicKey().deriveAddress()) + .account(keypair.publicKey().deriveAddress()) .nfTokenId(tokenId) .fee(XrpCurrencyAmount.ofDrops(50)) .sequence(accountInfoResult.accountData().sequence().plus(UnsignedInteger.ONE)) .amount(XrpCurrencyAmount.ofDrops(1000)) - .signingPublicKey(wallet.publicKey()) + .signingPublicKey(keypair.publicKey()) .flags(NfTokenCreateOfferFlags.SELL_NFTOKEN) .build(); SingleSignedTransaction signedOffer = signatureService.sign( - wallet.privateKey(), + keypair.privateKey(), nfTokenCreateSellOffer ); SubmitResult nfTokenCreateSellOfferSubmitResult = xrplClient.submit(signedOffer); @@ -720,11 +675,11 @@ void acceptOfferDirectModeWithBrokerFee() throws JsonRpcClientErrorException, Js logger.info("NFT Create Offer (Sell) transaction was validated successfully."); this.scanForResult( - () -> this.getValidatedAccountObjects(wallet.publicKey().deriveAddress()), + () -> this.getValidatedAccountObjects(keypair.publicKey().deriveAddress()), objectsResult -> objectsResult.accountObjects().stream() .anyMatch(object -> NfTokenOfferObject.class.isAssignableFrom(object.getClass()) && - ((NfTokenOfferObject) object).owner().equals(wallet.publicKey().deriveAddress()) + ((NfTokenOfferObject) object).owner().equals(keypair.publicKey().deriveAddress()) ) ); logger.info("NFTokenOffer object was found in account's objects."); @@ -735,24 +690,24 @@ void acceptOfferDirectModeWithBrokerFee() throws JsonRpcClientErrorException, Js assertThat(nftSellOffersResult.offers().size()).isEqualTo(1); // create buy offer from another account - KeyPair wallet2 = createRandomAccountEd25519(); + KeyPair keypair2 = createRandomAccountEd25519(); AccountInfoResult accountInfoResult2 = this.scanForResult( - () -> this.getValidatedAccountInfo(wallet2.publicKey().deriveAddress()) + () -> this.getValidatedAccountInfo(keypair2.publicKey().deriveAddress()) ); NfTokenCreateOffer nfTokenCreateOffer = NfTokenCreateOffer.builder() - .account(wallet2.publicKey().deriveAddress()) - .owner(wallet.publicKey().deriveAddress()) + .account(keypair2.publicKey().deriveAddress()) + .owner(keypair.publicKey().deriveAddress()) .nfTokenId(tokenId) .fee(XrpCurrencyAmount.ofDrops(50)) .sequence(accountInfoResult2.accountData().sequence()) .amount(XrpCurrencyAmount.ofDrops(1000)) - .signingPublicKey(wallet2.publicKey()) + .signingPublicKey(keypair2.publicKey()) .build(); SingleSignedTransaction signedOffer2 = signatureService.sign( - wallet2.privateKey(), + keypair2.privateKey(), nfTokenCreateOffer ); SubmitResult nfTokenCreateOfferSubmitResult = xrplClient.submit(signedOffer2); @@ -769,11 +724,11 @@ void acceptOfferDirectModeWithBrokerFee() throws JsonRpcClientErrorException, Js logger.info("NFT Create Offer (Buy) transaction was validated successfully."); this.scanForResult( - () -> this.getValidatedAccountObjects(wallet2.publicKey().deriveAddress()), + () -> this.getValidatedAccountObjects(keypair2.publicKey().deriveAddress()), objectsResult -> objectsResult.accountObjects().stream() .anyMatch(object -> NfTokenOfferObject.class.isAssignableFrom(object.getClass()) && - ((NfTokenOfferObject) object).owner().equals(wallet2.publicKey().deriveAddress()) + ((NfTokenOfferObject) object).owner().equals(keypair2.publicKey().deriveAddress()) ) ); logger.info("NFTokenOffer object was found in account's objects."); @@ -814,17 +769,64 @@ void acceptOfferDirectModeWithBrokerFee() throws JsonRpcClientErrorException, Js ) ); - this.scanForResult( + scanForNfToken(keypair2, uri); + logger.info("NFT was transferred successfully."); + } + + private void assertEntryEqualsObjectFromAccountObjects(Address owner, NfTokenObject nfToken) + throws JsonRpcClientErrorException { + Optional maybeNfTokenPage = xrplClient.accountObjects( + AccountObjectsRequestParams.of(owner) + ).accountObjects().stream() + .filter(object -> NfTokenPageObject.class.isAssignableFrom(object.getClass())) + .map(object -> (NfTokenPageObject) object) + .findFirst(); + + assertThat(maybeNfTokenPage).isNotEmpty(); + NfTokenPageObject nfTokenPageObject = maybeNfTokenPage.get(); + assertThat(nfTokenPageObject.nfTokens()).contains( + NfTokenWrapper.of( + NfToken.builder() + .nfTokenId(nfToken.nfTokenId()) + .uri(nfToken.uri()) + .build() + ) + ); + + LedgerEntryResult entry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.nftPage(nfTokenPageObject.index(), LedgerSpecifier.CURRENT) + ); + + assertThat(entry.node()).isEqualTo(nfTokenPageObject); + + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(nfTokenPageObject.index(), NfTokenPageObject.class, LedgerSpecifier.CURRENT) + ); + + assertThat(entryByIndex.node()).isEqualTo(entry.node()); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(nfTokenPageObject.index(), LedgerSpecifier.CURRENT) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); + } + + private NfTokenObject scanForNfToken(KeyPair minterKeyPair, NfTokenUri uri) { + return this.scanForResult( () -> { try { - return xrplClient.accountNfts(wallet2.publicKey().deriveAddress()); + return xrplClient.accountNfts(minterKeyPair.publicKey().deriveAddress()); } catch (JsonRpcClientErrorException e) { + logger.error("Exception occurred while getting account nfts: {}", e.getMessage(), e); throw new RuntimeException(e); } }, result -> result.accountNfts().stream() .anyMatch(nft -> nft.uri().get().equals(uri)) - ); - logger.info("NFT was transferred successfully."); + ).accountNfts() + .stream().filter(nft -> nft.uri().get().equals(uri)) + .findFirst() + .get(); } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/OfferIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/OfferIT.java index 6b54508a8..7cc13c93c 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/OfferIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/OfferIT.java @@ -36,12 +36,17 @@ import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; +import org.xrpl.xrpl4j.model.client.ledger.OfferLedgerEntryParams; import org.xrpl.xrpl4j.model.client.path.BookOffersRequestParams; import org.xrpl.xrpl4j.model.client.path.BookOffersResult; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; import org.xrpl.xrpl4j.model.flags.OfferCreateFlags; import org.xrpl.xrpl4j.model.flags.OfferFlags; +import org.xrpl.xrpl4j.model.ledger.EscrowObject; import org.xrpl.xrpl4j.model.ledger.Issue; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.ledger.OfferObject; import org.xrpl.xrpl4j.model.ledger.RippleStateObject; import org.xrpl.xrpl4j.model.transactions.Address; @@ -197,45 +202,9 @@ public void createOpenOfferAndCancel() throws JsonRpcClientErrorException, JsonP assertThat(offerObject.takerGets()).isEqualTo(offerCreate.takerGets()); assertThat(offerObject.takerPays()).isEqualTo(offerCreate.takerPays()); - cancelOffer(purchaser, offerObject.sequence(), "tesSUCCESS"); - } - - /** - * Cancels an offer and verifies the offer no longer exists on ledger for the account. - * - * @param purchaser The {@link KeyPair} of the buyer. - * @param offerSequence The sequence of the offer. - * - * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. - */ - private void cancelOffer( - KeyPair purchaser, - UnsignedInteger offerSequence, - String expectedResult - ) throws JsonRpcClientErrorException, JsonProcessingException { - AccountInfoResult infoResult = this.scanForResult( - () -> this.getValidatedAccountInfo(purchaser.publicKey().deriveAddress())); - UnsignedInteger nextSequence = infoResult.accountData().sequence(); - - FeeResult feeResult = xrplClient.fee(); - - OfferCancel offerCancel = OfferCancel.builder() - .account(purchaser.publicKey().deriveAddress()) - .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) - .sequence(nextSequence) - .offerSequence(offerSequence) - .signingPublicKey(purchaser.publicKey()) - .build(); - - SingleSignedTransaction signedOfferCancel = signatureService.sign( - purchaser.privateKey(), offerCancel - ); - SubmitResult cancelResponse = xrplClient.submit(signedOfferCancel); - assertThat(cancelResponse.engineResult()).isEqualTo(expectedResult); + assertThatEntryEqualsObjectFromAccountObjects(offerObject); - assertEmptyResults(() -> this.getValidatedAccountObjects(purchaser.publicKey().deriveAddress(), OfferObject.class)); - assertEmptyResults( - () -> this.getValidatedAccountObjects(purchaser.publicKey().deriveAddress(), RippleStateObject.class)); + cancelOffer(purchaser, offerObject.sequence(), "tesSUCCESS"); } @Test @@ -289,17 +258,6 @@ public void createUnmatchedKillOrFill() throws JsonRpcClientErrorException, Json () -> this.getValidatedAccountObjects(purchaser.publicKey().deriveAddress(), RippleStateObject.class)); } - /** - * Asserts the supplier returns empty results, waiting up to 10 seconds for that condition to be true. - * - * @param supplier results supplier. - */ - private void assertEmptyResults(Supplier> supplier) { - Awaitility.await() - .atMost(Durations.TEN_SECONDS) - .until(supplier::get, Matchers.empty()); - } - @Test public void createFullyMatchedOffer() throws JsonRpcClientErrorException, JsonProcessingException { // GIVEN a buy offer that has a really great exchange rate @@ -404,4 +362,77 @@ public RippleStateObject scanForIssuedCurrency(KeyPair purchaser, String currenc .orElse(null)); } + + private void assertThatEntryEqualsObjectFromAccountObjects(OfferObject offerObject) + throws JsonRpcClientErrorException { + LedgerEntryResult entry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.offer( + OfferLedgerEntryParams.builder() + .account(offerObject.account()) + .seq(offerObject.sequence()) + .build(), + LedgerSpecifier.VALIDATED + ) + ); + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(offerObject.index(), OfferObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entry.node()); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(offerObject.index(), LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); + } + + /** + * Asserts the supplier returns empty results, waiting up to 10 seconds for that condition to be true. + * + * @param supplier results supplier. + */ + private void assertEmptyResults(Supplier> supplier) { + Awaitility.await() + .atMost(Durations.TEN_SECONDS) + .until(supplier::get, Matchers.empty()); + } + + /** + * Cancels an offer and verifies the offer no longer exists on ledger for the account. + * + * @param purchaser The {@link KeyPair} of the buyer. + * @param offerSequence The sequence of the offer. + * + * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. + */ + private void cancelOffer( + KeyPair purchaser, + UnsignedInteger offerSequence, + String expectedResult + ) throws JsonRpcClientErrorException, JsonProcessingException { + AccountInfoResult infoResult = this.scanForResult( + () -> this.getValidatedAccountInfo(purchaser.publicKey().deriveAddress())); + UnsignedInteger nextSequence = infoResult.accountData().sequence(); + + FeeResult feeResult = xrplClient.fee(); + + OfferCancel offerCancel = OfferCancel.builder() + .account(purchaser.publicKey().deriveAddress()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) + .sequence(nextSequence) + .offerSequence(offerSequence) + .signingPublicKey(purchaser.publicKey()) + .build(); + + SingleSignedTransaction signedOfferCancel = signatureService.sign( + purchaser.privateKey(), offerCancel + ); + SubmitResult cancelResponse = xrplClient.submit(signedOfferCancel); + assertThat(cancelResponse.engineResult()).isEqualTo(expectedResult); + + assertEmptyResults(() -> this.getValidatedAccountObjects(purchaser.publicKey().deriveAddress(), OfferObject.class)); + assertEmptyResults( + () -> this.getValidatedAccountObjects(purchaser.publicKey().deriveAddress(), RippleStateObject.class)); + } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/PaymentChannelIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/PaymentChannelIT.java index 3e3fb0994..6ddfc914a 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/PaymentChannelIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/PaymentChannelIT.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; import org.xrpl.xrpl4j.crypto.keys.KeyPair; @@ -40,7 +41,12 @@ import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; +import org.xrpl.xrpl4j.model.client.ledger.OfferLedgerEntryParams; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; +import org.xrpl.xrpl4j.model.ledger.OfferObject; import org.xrpl.xrpl4j.model.ledger.PayChannelObject; import org.xrpl.xrpl4j.model.transactions.PaymentChannelClaim; import org.xrpl.xrpl4j.model.transactions.PaymentChannelCreate; @@ -114,14 +120,8 @@ public void createPaymentChannel() throws JsonRpcClientErrorException, JsonProce ////////////////////////// // Also validate that the channel exists in the account's objects - scanForResult( - () -> getValidatedAccountObjects(sourceKeyPair.publicKey().deriveAddress()), - objectsResult -> objectsResult.accountObjects().stream() - .anyMatch(object -> - PayChannelObject.class.isAssignableFrom(object.getClass()) && - ((PayChannelObject) object).destination().equals(destinationKeyPair.publicKey().deriveAddress()) - ) - ); + PayChannelObject payChannelObject = scanForPayChannelObject(sourceKeyPair, destinationKeyPair); + assertThatEntryEqualsObjectFromAccountObjects(payChannelObject); ////////////////////////// // Validate that the amount of the payment channel was deducted from the source @@ -194,6 +194,9 @@ void createAndClaimPaymentChannel() throws JsonRpcClientErrorException, JsonProc assertThat(paymentChannel.publicKeyHex()).isNotEmpty().get().isEqualTo(paymentChannelCreate.publicKey()); assertThat(paymentChannel.cancelAfter()).isNotEmpty().get().isEqualTo(paymentChannelCreate.cancelAfter().get()); + PayChannelObject payChannelObject = scanForPayChannelObject(sourceKeyPair, destinationKeyPair); + assertThatEntryEqualsObjectFromAccountObjects(payChannelObject); + AccountInfoResult destinationAccountInfo = scanForResult( () -> getValidatedAccountInfo(destinationKeyPair.publicKey().deriveAddress()) ); @@ -253,6 +256,9 @@ void createAndClaimPaymentChannel() throws JsonRpcClientErrorException, JsonProc .plus(paymentChannelClaim.balance().get()) ) ); + + PayChannelObject payChannelObjectAfterClaim = scanForPayChannelObject(sourceKeyPair, destinationKeyPair); + assertThatEntryEqualsObjectFromAccountObjects(payChannelObjectAfterClaim); } @Test @@ -312,6 +318,9 @@ void createAddFundsAndSetExpirationToPaymentChannel() throws JsonRpcClientErrorE assertThat(paymentChannel.publicKeyHex()).isNotEmpty().get().isEqualTo(paymentChannelCreate.publicKey()); assertThat(paymentChannel.cancelAfter()).isNotEmpty().get().isEqualTo(paymentChannelCreate.cancelAfter().get()); + PayChannelObject payChannelObject = scanForPayChannelObject(sourceKeyPair, destinationKeyPair); + assertThatEntryEqualsObjectFromAccountObjects(payChannelObject); + PaymentChannelFund paymentChannelFund = PaymentChannelFund.builder() .account(sourceKeyPair.publicKey().deriveAddress()) .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) @@ -345,6 +354,9 @@ void createAddFundsAndSetExpirationToPaymentChannel() throws JsonRpcClientErrorE ) ); + PayChannelObject payChannelObjectAfterFund = scanForPayChannelObject(sourceKeyPair, destinationKeyPair); + assertThatEntryEqualsObjectFromAccountObjects(payChannelObjectAfterFund); + ////////////////////////// // Then set a new expiry on the channel by submitting a PaymentChannelFund // transaction with an expiration and 1 drop of XRP in the amount field @@ -386,6 +398,9 @@ void createAddFundsAndSetExpirationToPaymentChannel() throws JsonRpcClientErrorE channel.expiration().get().equals(newExpiry) ) ); + + PayChannelObject payChannelObjectAfterExpiryBump = scanForPayChannelObject(sourceKeyPair, destinationKeyPair); + assertThatEntryEqualsObjectFromAccountObjects(payChannelObjectAfterExpiryBump); } @Test @@ -449,4 +464,41 @@ void testCurrentAccountChannels() throws JsonRpcClientErrorException, JsonProces assertThat(accountChannelsResult.ledgerIndex()).isEmpty(); assertThat(accountChannelsResult.ledgerCurrentIndex()).isNotEmpty(); } + + private PayChannelObject scanForPayChannelObject(KeyPair sourceKeyPair, KeyPair destinationKeyPair) { + return (PayChannelObject) scanForResult( + () -> getValidatedAccountObjects(sourceKeyPair.publicKey().deriveAddress()), + objectsResult -> objectsResult.accountObjects().stream() + .anyMatch(object -> + PayChannelObject.class.isAssignableFrom(object.getClass()) && + ((PayChannelObject) object).destination().equals(destinationKeyPair.publicKey().deriveAddress()) + ) + ).accountObjects().stream() + .filter(object -> PayChannelObject.class.isAssignableFrom(object.getClass()) && + ((PayChannelObject) object).destination().equals(destinationKeyPair.publicKey().deriveAddress())) + .findFirst() + .get(); + } + + private void assertThatEntryEqualsObjectFromAccountObjects(PayChannelObject payChannelObject) + throws JsonRpcClientErrorException { + LedgerEntryResult entry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.paymentChannel( + payChannelObject.index(), + LedgerSpecifier.VALIDATED + ) + ); + + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(payChannelObject.index(), PayChannelObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entry.node()); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(payChannelObject.index(), LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); + } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SetRegularKeyIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SetRegularKeyIT.java index 218335ff9..d96f1140e 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SetRegularKeyIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SetRegularKeyIT.java @@ -30,9 +30,14 @@ import org.xrpl.xrpl4j.crypto.keys.Seed; import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; +import org.xrpl.xrpl4j.model.ledger.AccountRootObject; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.transactions.AccountSet; import org.xrpl.xrpl4j.model.transactions.SetRegularKey; @@ -50,6 +55,7 @@ void setRegularKeyOnAccount() throws JsonRpcClientErrorException, JsonProcessing ////////////////////////// // Wait for the account to show up on ledger AccountInfoResult accountInfo = scanForResult(() -> getValidatedAccountInfo(wallet.publicKey().deriveAddress())); + assertEntryEqualsAccountInfo(wallet, accountInfo); ////////////////////////// // Generate a new wallet locally @@ -100,6 +106,10 @@ void setRegularKeyOnAccount() throws JsonRpcClientErrorException, JsonProcessing } ); + AccountInfoResult accountInfoAfterRegKeySet = scanForResult( + () -> getValidatedAccountInfo(wallet.publicKey().deriveAddress()) + ); + assertEntryEqualsAccountInfo(wallet, accountInfoAfterRegKeySet); } @Test @@ -111,6 +121,7 @@ void removeRegularKeyFromAccount() throws JsonRpcClientErrorException, JsonProce ////////////////////////// // Wait for the account to show up on ledger AccountInfoResult accountInfo = scanForResult(() -> getValidatedAccountInfo(wallet.publicKey().deriveAddress())); + assertEntryEqualsAccountInfo(wallet, accountInfo); ////////////////////////// // Generate a new wallet locally @@ -162,6 +173,11 @@ void removeRegularKeyFromAccount() throws JsonRpcClientErrorException, JsonProce } ); + AccountInfoResult accountInfoAfterSet = scanForResult( + () -> getValidatedAccountInfo(wallet.publicKey().deriveAddress()) + ); + assertEntryEqualsAccountInfo(wallet, accountInfoAfterSet); + SetRegularKey removeRegularKey = SetRegularKey.builder() .account(wallet.publicKey().deriveAddress()) .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) @@ -178,9 +194,34 @@ void removeRegularKeyFromAccount() throws JsonRpcClientErrorException, JsonProce removeResult.transactionResult().hash() ); - scanForResult( + AccountInfoResult accountInfoAfterRemoving = scanForResult( () -> getValidatedAccountInfo(wallet.publicKey().deriveAddress()), infoResult -> !infoResult.accountData().regularKey().isPresent() ); + + assertEntryEqualsAccountInfo(wallet, accountInfoAfterRemoving); + } + + private void assertEntryEqualsAccountInfo( + KeyPair keyPair, + AccountInfoResult accountInfo + ) throws JsonRpcClientErrorException { + LedgerEntryResult accountRoot = xrplClient.ledgerEntry( + LedgerEntryRequestParams.accountRoot(keyPair.publicKey().deriveAddress(), LedgerSpecifier.VALIDATED) + ); + + assertThat(accountInfo.accountData()).isEqualTo(accountRoot.node()); + + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(accountRoot.index(), AccountRootObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(accountRoot.node()); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(accountRoot.index(), LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SignerListSetIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SignerListSetIT.java index e7dd423e3..36b7d7bea 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SignerListSetIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SignerListSetIT.java @@ -31,16 +31,26 @@ import org.xrpl.xrpl4j.crypto.signing.MultiSignedTransaction; import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; import org.xrpl.xrpl4j.model.client.transactions.SubmitMultiSignedResult; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; +import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; +import org.xrpl.xrpl4j.model.ledger.AccountRootObject; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.ledger.SignerEntry; import org.xrpl.xrpl4j.model.ledger.SignerEntryWrapper; +import org.xrpl.xrpl4j.model.ledger.SignerListObject; +import org.xrpl.xrpl4j.model.transactions.Hash256; import org.xrpl.xrpl4j.model.transactions.Payment; import org.xrpl.xrpl4j.model.transactions.Signer; import org.xrpl.xrpl4j.model.transactions.SignerListSet; import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.metadata.AffectedNode; +import org.xrpl.xrpl4j.model.transactions.metadata.MetaLedgerEntryType; import java.util.Collections; import java.util.Comparator; @@ -118,6 +128,8 @@ void addSignersToSignerListAndSendPayment() throws JsonRpcClientErrorException, infoResult -> infoResult.accountData().signerLists().size() == 1 ); + assertSignerListEntryEqualsAccountInfo(signedSignerListSet.hash(), sourceAccountInfoAfterSignerListSet); + assertThat( sourceAccountInfoAfterSignerListSet.accountData().signerLists().get(0) .signerEntries().stream() @@ -226,6 +238,8 @@ void addSignersToSignerListThenDeleteSignerList() throws JsonRpcClientErrorExcep infoResult -> infoResult.accountData().signerLists().size() == 1 ); + assertSignerListEntryEqualsAccountInfo(signedSignerListSet.hash(), sourceAccountInfoAfterSignerListSet); + assertThat( sourceAccountInfoAfterSignerListSet.accountData().signerLists().get(0) .signerEntries().stream() @@ -263,4 +277,30 @@ void addSignersToSignerListThenDeleteSignerList() throws JsonRpcClientErrorExcep ); } + + private void assertSignerListEntryEqualsAccountInfo(Hash256 signerListSetTx, AccountInfoResult accountInfo) + throws JsonRpcClientErrorException { + + TransactionResult signerListSet = this.getValidatedTransaction(signerListSetTx, + SignerListSet.class); + Hash256 signerListId = signerListSet.metadata().get() + .affectedNodes() + .stream() + .filter(affectedNode -> affectedNode.ledgerEntryType().equals(MetaLedgerEntryType.SIGNER_LIST)) + .findFirst() + .map(AffectedNode::ledgerIndex) + .get(); + + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(signerListId, SignerListObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(accountInfo.accountData().signerLists().get(0)); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(signerListId, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); + } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitPaymentIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitPaymentIT.java index 135500e18..5ee2a97bf 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitPaymentIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitPaymentIT.java @@ -85,13 +85,9 @@ public void sendPayment() throws JsonRpcClientErrorException, JsonProcessingExce @Test public void sendPaymentFromSecp256k1KeyPair() throws JsonRpcClientErrorException, JsonProcessingException { - KeyPair senderKeyPair = Seed.fromBase58EncodedSecret( - Base58EncodedSecret.of("sp5fghtJtpUorTwvof1NpDXAzNwf5") - ).deriveKeyPair(); + KeyPair senderKeyPair = this.createRandomAccountSecp256k1(); logger.info("Generated source testnet wallet with address " + senderKeyPair.publicKey().deriveAddress()); - fundAccount(senderKeyPair.publicKey().deriveAddress()); - KeyPair destinationKeyPair = createRandomAccountEd25519(); FeeResult feeResult = xrplClient.fee(); diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TicketIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TicketIT.java index 15fa9e1c8..d4531f336 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TicketIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TicketIT.java @@ -29,12 +29,20 @@ import org.xrpl.xrpl4j.crypto.keys.KeyPair; import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.fees.FeeResult; import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; +import org.xrpl.xrpl4j.model.ledger.SignerListObject; import org.xrpl.xrpl4j.model.ledger.TicketObject; import org.xrpl.xrpl4j.model.transactions.AccountSet; +import org.xrpl.xrpl4j.model.transactions.Hash256; import org.xrpl.xrpl4j.model.transactions.TicketCreate; +import org.xrpl.xrpl4j.model.transactions.metadata.AffectedNode; +import org.xrpl.xrpl4j.model.transactions.metadata.MetaLedgerEntryType; import java.util.List; @@ -71,11 +79,17 @@ void createTicketAndUseSequenceNumber() throws JsonRpcClientErrorException, Json submitResult.transactionResult().hash() ); - this.scanForResult( + Hash256 ticketId = this.scanForResult( () -> this.getValidatedTransaction( submitResult.transactionResult().hash(), TicketCreate.class) - ); + ).metadata().get() + .affectedNodes() + .stream() + .filter(affectedNode -> affectedNode.ledgerEntryType().equals(MetaLedgerEntryType.TICKET)) + .findFirst() + .map(AffectedNode::ledgerIndex) + .get(); AccountInfoResult accountInfoAfterTicketCreate = getValidatedAccountInfo(sourceKeyPair.publicKey().deriveAddress()); assertThat(accountInfoAfterTicketCreate.accountData().ticketCount()).isNotEmpty().get() @@ -87,6 +101,18 @@ void createTicketAndUseSequenceNumber() throws JsonRpcClientErrorException, Json ); assertThat(tickets).asList().hasSize(1); + LedgerEntryResult entryByIndex = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(ticketId, TicketObject.class, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(tickets.get(0)); + + LedgerEntryResult entryByIndexUnTyped = xrplClient.ledgerEntry( + LedgerEntryRequestParams.index(ticketId, LedgerSpecifier.VALIDATED) + ); + + assertThat(entryByIndex.node()).isEqualTo(entryByIndexUnTyped.node()); + AccountSet accountSet = AccountSet.builder() .account(sourceKeyPair.publicKey().deriveAddress()) .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TransactUsingDerivedKeySignatureServiceIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TransactUsingDerivedKeySignatureServiceIT.java index 7fddaaa01..5f63b57d6 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TransactUsingDerivedKeySignatureServiceIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TransactUsingDerivedKeySignatureServiceIT.java @@ -49,6 +49,7 @@ import java.util.Comparator; import java.util.Objects; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; /** @@ -59,7 +60,15 @@ public class TransactUsingDerivedKeySignatureServiceIT extends AbstractIT { @Test public void sendPaymentFromEd25519Account() throws JsonRpcClientErrorException, JsonProcessingException { - final PrivateKeyReference sourceKeyMetadata = constructPrivateKeyReference("sourceWallet", KeyType.ED25519); + // We must use a random key identifier here rather than a hardcoded identifier because the keypair associated + // with the hard coded identifier is deterministic. When we run ITs on a real network in CI like testnet or devnet, + // we sometimes see `tefPAST_SEQ` errors when submitting the transaction because another CI job has submitted + // a transaction for the account between when this test gets the source's account info and when it submits the + // transaction. Using a random account every time ensures this test's behavior is isolated from other tests. + final PrivateKeyReference sourceKeyMetadata = constructPrivateKeyReference( + UUID.randomUUID().toString(), + KeyType.ED25519 + ); final PublicKey sourceWalletPublicKey = derivedKeySignatureService.derivePublicKey(sourceKeyMetadata); final Address sourceWalletAddress = sourceWalletPublicKey.deriveAddress(); this.fundAccount(sourceWalletAddress); @@ -94,7 +103,15 @@ public void sendPaymentFromEd25519Account() throws JsonRpcClientErrorException, @Test public void sendPaymentFromSecp256k1Account() throws JsonRpcClientErrorException, JsonProcessingException { - final PrivateKeyReference sourceKeyMetadata = constructPrivateKeyReference("sourceWallet", KeyType.SECP256K1); + // We must use a random key identifier here rather than a hardcoded identifier because the keypair associated + // with the hard coded identifier is deterministic. When we run ITs on a real network in CI like testnet or devnet, + // we sometimes see `tefPAST_SEQ` errors when submitting the transaction because another CI job has submitted + // a transaction for the account between when this test gets the source's account info and when it submits the + // transaction. Using a random account every time ensures this test's behavior is isolated from other tests. + final PrivateKeyReference sourceKeyMetadata = constructPrivateKeyReference( + UUID.randomUUID().toString(), + KeyType.SECP256K1 + ); final PublicKey sourceWalletPublicKey = derivedKeySignatureService.derivePublicKey(sourceKeyMetadata); final Address sourceWalletAddress = sourceWalletPublicKey.deriveAddress(); this.fundAccount(sourceWalletAddress); diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/environment/ReportingMainnetEnvironment.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/environment/ReportingMainnetEnvironment.java index 3445b63bc..115748171 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/environment/ReportingMainnetEnvironment.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/environment/ReportingMainnetEnvironment.java @@ -30,7 +30,7 @@ */ public class ReportingMainnetEnvironment extends MainnetEnvironment { - private final XrplClient xrplClient = new XrplClient(HttpUrl.parse("https://s2-reporting.ripple.com:51234")); + private final XrplClient xrplClient = new XrplClient(HttpUrl.parse("https://xrplcluster.com")); @Override public XrplClient getXrplClient() { diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/environment/RippledContainer.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/environment/RippledContainer.java index 947d5f1d7..d5f5484b9 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/environment/RippledContainer.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/environment/RippledContainer.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. @@ -112,6 +112,7 @@ public RippledContainer start(int acceptIntervalMillis) { } started = true; rippledContainer.start(); + // rippled is run in standalone mode which means that ledgers won't automatically close. You have to manually // advance the ledger using the "ledger_accept" method on the admin API. To mimic the behavior of a networked // rippled, run a scheduled task to trigger the "ledger_accept" method. diff --git a/xrpl4j-integration-tests/src/test/resources/rippled/rippled.cfg b/xrpl4j-integration-tests/src/test/resources/rippled/rippled.cfg index 5947fe6d9..8b9565494 100644 --- a/xrpl4j-integration-tests/src/test/resources/rippled/rippled.cfg +++ b/xrpl4j-integration-tests/src/test/resources/rippled/rippled.cfg @@ -1284,3 +1284,7 @@ fixTrustLinesToSelf fixUniversalNumber ImmediateOfferKilled XRPFees +fixNFTokenRemint +fixReducedOffersV1 +Clawback +AMM \ No newline at end of file