diff --git a/pom.xml b/pom.xml index e351a49ba..3ab480174 100644 --- a/pom.xml +++ b/pom.xml @@ -117,7 +117,7 @@ org.junit junit-bom - 5.13.1 + 5.14.1 pom import @@ -175,19 +175,14 @@ org.awaitility awaitility - 4.2.0 - test - - - org.awaitility - awaitility-proxy - 3.1.6 + 4.3.0 test com.github.docker-java docker-java-api - 3.3.4 + 3.7.0 + test org.hamcrest diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureService.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureService.java index c1d1875c8..420f1de20 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureService.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureService.java @@ -113,11 +113,6 @@ public Signature signInner(final P privateKeyable, final Batch batchTransaction) return this.abstractTransactionSigner.signInner(privateKeyable, batchTransaction); } - @Override - public SingleSignedTransaction signOuter(final P privateKeyable, final Batch batchTransaction) { - return this.abstractTransactionSigner.signOuter(privateKeyable, batchTransaction); - } - @Override public Signature multiSign(final P privateKeyable, final T transaction) { return abstractTransactionSigner.multiSign(privateKeyable, transaction); @@ -128,11 +123,6 @@ public Signature multiSignInner(final P privateKeyable, final Batch batchTransac return abstractTransactionSigner.multiSignInner(privateKeyable, batchTransaction); } - @Override - public Signature multiSignOuter(final P privateKeyable, final Batch batchTransaction) { - return abstractTransactionSigner.multiSignOuter(privateKeyable, batchTransaction); - } - @Override public boolean verify(final Signer signer, final T unsignedTransaction) { return abstractTransactionVerifier.verify(signer, unsignedTransaction); 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 94eb2b33f..6e8280c29 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 @@ -20,7 +20,6 @@ * =========================LICENSE_END================================== */ -import com.google.common.annotations.Beta; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.crypto.keys.PrivateKeyReference; import org.xrpl.xrpl4j.crypto.keys.PrivateKeyable; @@ -56,11 +55,6 @@ public SingleSignedTransaction sign(final P privateKe Objects.requireNonNull(privateKeyable); Objects.requireNonNull(transaction); - // The Batch transaction requires special handling when signing, so disallow it from this method. - if (Batch.class.isAssignableFrom(transaction.getClass())) { - throw new RuntimeException("Batch transactions must be signed using signInner() or signOuter()"); - } - final UnsignedByteArray signableTransactionBytes = this.signatureUtils.toSignableBytes(transaction); final Signature signature = this.signingHelper(privateKeyable, signableTransactionBytes); return this.signatureUtils.addSignatureToTransaction(transaction, signature); @@ -92,24 +86,11 @@ public Signature signInner(final P privateKeyable, final Batch batchTransaction) return this.signingHelper(privateKeyable, signableBytes); } - @Override - public SingleSignedTransaction signOuter(final P privateKeyable, final Batch batchTransaction) { - Objects.requireNonNull(privateKeyable); - Objects.requireNonNull(batchTransaction); - final UnsignedByteArray signableBytes = this.signatureUtils.toSignableBytes(batchTransaction); - final Signature signature = this.signingHelper(privateKeyable, signableBytes); - return this.signatureUtils.addSignatureToTransaction(batchTransaction, signature); - } - @Override public Signature multiSign(final P privateKeyable, final T transaction) { Objects.requireNonNull(privateKeyable); Objects.requireNonNull(transaction); - if (Batch.class.isAssignableFrom(transaction.getClass())) { - throw new RuntimeException("Batch transactions must be signed using multiSignInner() or multiSignOuter()"); - } - final Address address = derivePublicKey(privateKeyable).deriveAddress(); final UnsignedByteArray signableTransactionBytes = this.signatureUtils.toMultiSignableBytes(transaction, address); return this.signingHelper(privateKeyable, signableTransactionBytes); @@ -126,17 +107,6 @@ public Signature multiSignInner(final P privateKeyable, final Batch batchTransac return this.signingHelper(privateKeyable, signableBytes); } - @Override - public Signature multiSignOuter(final P privateKeyable, final Batch batchTransaction) { - Objects.requireNonNull(privateKeyable); - Objects.requireNonNull(batchTransaction); - - final Address address = derivePublicKey(privateKeyable).deriveAddress(); - final UnsignedByteArray signableBytes = this.signatureUtils.toMultiSignableBytes(batchTransaction, address); - - return this.signingHelper(privateKeyable, signableBytes); - } - /** * Helper to generate a signature based upon an {@link UnsignedByteArray} of transaction bytes. * diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/TransactionSigner.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/TransactionSigner.java index 62f302196..e872fe8e9 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/TransactionSigner.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/TransactionSigner.java @@ -38,7 +38,7 @@ public interface TransactionSigner

{ /** * Accessor for the public-key corresponding to the supplied key meta-data. This method exists to support - * implementations that hold private-key material internally, yet need a way for external callers to determine the + * implementations that hold private-key material internally yet need a way for external callers to determine the * actual public key for signature verification or other purposes. * * @param privateKeyable A {@link PrivateKeyable} to derive a public key from. @@ -48,8 +48,7 @@ public interface TransactionSigner

{ PublicKey derivePublicKey(P privateKeyable); /** - * Obtain a singly-signed signature for the supplied transaction using {@code privateKeyable} and the single-sign - * mechanism. + * Get a transaction with a single-sig using {@code privateKeyable}. * * @param privateKeyable The {@link P} used to sign {@code transaction}. * @param transaction The {@link Transaction} to sign. @@ -84,7 +83,7 @@ public interface TransactionSigner

{ Signature sign(P privateKeyable, Attestation attestation); /** - * Obtain a signature for a batch transaction using the supplied {@link P}. + * Get a signature for a batch transaction using the supplied {@link P}. * *

Per XLS-0056, BatchSigners sign a specific format: HashPrefix::batch + flags + count + inner tx IDs. * This differs from both single-signing and multi-signing.

@@ -100,23 +99,6 @@ public interface TransactionSigner

{ @Beta Signature signInner(P privateKeyable, Batch batchTransaction); - /** - * Obtain a signature for a batch transaction using the supplied {@link P}. - * - *

Per XLS-0056, BatchSigners sign a specific format: HashPrefix::batch + flags + count + inner tx IDs. - * This differs from both single-signing and multi-signing.

- * - *

This method will be marked {@link Beta} until the featureBatch amendment is enabled on mainnet. - * Its API is subject to change.

- * - * @param privateKeyable The {@link P} used to sign {@code batchTransaction}. - * @param batchTransaction The {@link Batch} transaction to sign. - * - * @return A {@link Signature} for the batch transaction. - */ - @Beta - SingleSignedTransaction signOuter(P privateKeyable, Batch batchTransaction); - /** * Get a signature for the supplied unsigned transaction using the supplied {@link P}. The primary reason this * method's signature diverges from {@link #sign(PrivateKeyable, Transaction)} is that for multi-sign scenarios, the @@ -151,8 +133,6 @@ public interface TransactionSigner

{ */ Signature multiSignInner(P privateKeyable, Batch batchTransaction); - Signature multiSignOuter(P privateKeyable, Batch batchTransaction); - /** * Obtain a signature for the supplied unsigned transaction using the supplied {@link P}. *

@@ -177,16 +157,19 @@ public interface TransactionSigner

{ * @param The type of the transaction to be signed. * * @return A {@link Signature} for the transaction. + * + * @deprecated Use {@link #multiSign(PrivateKeyable, Transaction)} instead and assemble a {@link Signer} manually. + * This will allow callers to better manage public-key derivation, especially for derived key scenarios like an HSM + * where public-key derivation is expensive and may only need to be done once for multiple multi-sig operations. */ + @Deprecated default Signer multiSignToSigner(P privateKeyable, T transaction) { Objects.requireNonNull(privateKeyable); Objects.requireNonNull(transaction); // Compute this only once, just in case public-key derivation is expensive (e.g., a remote HSM). final PublicKey signingPublicKey = this.derivePublicKey(privateKeyable); - return Signer.builder() - .signingPublicKey(signingPublicKey) - .transactionSignature(multiSign(privateKeyable, transaction)) - .build(); + return Signer.builder().signingPublicKey(signingPublicKey) + .transactionSignature(multiSign(privateKeyable, transaction)).build(); } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/BcDerivedKeySignatureService.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/BcDerivedKeySignatureService.java index 0ea9092a6..6a9d7dc79 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/BcDerivedKeySignatureService.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/bc/BcDerivedKeySignatureService.java @@ -150,14 +150,6 @@ public Signature signInner(final PrivateKeyReference privateKeyable, final Batch return getTransactionSigner(privateKeyable).signInner(batchTransaction); } - @Override - public SingleSignedTransaction signOuter(final PrivateKeyReference privateKeyable, - final Batch batchTransaction) { - Objects.requireNonNull(privateKeyable); - Objects.requireNonNull(batchTransaction); - return getTransactionSigner(privateKeyable).signOuter(batchTransaction); - } - @Override public Signature multiSignInner(final PrivateKeyReference privateKeyable, final Batch batchTransaction) { Objects.requireNonNull(privateKeyable); @@ -165,13 +157,6 @@ public Signature multiSignInner(final PrivateKeyReference privateKeyable, final return getTransactionSigner(privateKeyable).multiSignInner(batchTransaction); } - @Override - public Signature multiSignOuter(final PrivateKeyReference privateKeyable, final Batch batchTransaction) { - Objects.requireNonNull(privateKeyable); - Objects.requireNonNull(batchTransaction); - return getTransactionSigner(privateKeyable).multiSignOuter(batchTransaction); - } - @Override public Signer multiSignToSigner(PrivateKeyReference privateKeyable, T transaction) { return getTransactionSigner(privateKeyable).multiSignToSigner(transaction); @@ -344,6 +329,8 @@ public Signature multiSign(final T transaction) { return bcSignatureService.multiSign(this.privateKey, transaction); } + // @deprecated See comment in `TransactionSigner#multiSignToSigner`. + @Deprecated public Signer multiSignToSigner(T transaction) { return bcSignatureService.multiSignToSigner(this.privateKey, transaction); } @@ -352,18 +339,10 @@ public final Signature signInner(final Batch transaction) { return bcSignatureService.signInner(this.privateKey, transaction); } - public final SingleSignedTransaction signOuter(final Batch transaction) { - return bcSignatureService.signOuter(this.privateKey, transaction); - } - public final Signature multiSignInner(final Batch transaction) { return bcSignatureService.multiSignInner(this.privateKey, transaction); } - public final Signature multiSignOuter(final Batch transaction) { - return bcSignatureService.multiSignOuter(this.privateKey, transaction); - } - public PublicKey getPublicKey() { return BcKeyUtils.toPublicKey(this.privateKey); } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/accounts/AccountTransactionsTransaction.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/accounts/AccountTransactionsTransaction.java index abf4d0e51..7401fde17 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/accounts/AccountTransactionsTransaction.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/accounts/AccountTransactionsTransaction.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. @@ -33,8 +33,6 @@ import org.xrpl.xrpl4j.model.transactions.Hash256; import org.xrpl.xrpl4j.model.transactions.Transaction; -import java.time.Instant; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Optional; @@ -50,7 +48,7 @@ using = AccountTransactionsTransactionDeserializer.class ) public interface AccountTransactionsTransaction { - + /** * Construct a builder for this class. * @@ -86,8 +84,8 @@ static ImmutableAccountTransactionsTransaction.Builder ImmutableAccountTransactionsTransaction.Builder closeDate(); /** - * The approximate close time in UTC offset. - * This is derived from undocumented field. + * The approximate close time in UTC offset. This is derived from undocumented field. * * @return An optionally-present {@link ZonedDateTime}. */ @@ -105,5 +102,5 @@ static ImmutableAccountTransactionsTransaction.Builder closeDateHuman() { return closeDate().map(TimeUtils::xrplTimeToZonedDateTime); } - + } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/common/TimeUtils.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/common/TimeUtils.java index 8ce5913d9..868b951e6 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/common/TimeUtils.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/common/TimeUtils.java @@ -3,7 +3,7 @@ import com.google.common.primitives.UnsignedLong; import java.time.Instant; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; /** @@ -25,7 +25,7 @@ public class TimeUtils { * @return A {@link ZonedDateTime} in UTC. */ public static ZonedDateTime xrplTimeToZonedDateTime(UnsignedLong xrplTime) { - return Instant.ofEpochSecond(RIPPLE_EPOCH.plus(xrplTime).longValue()).atZone(ZoneId.of("UTC")); + return Instant.ofEpochSecond(RIPPLE_EPOCH.plus(xrplTime).longValue()).atZone(ZoneOffset.UTC); } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResult.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResult.java index ce5476d3b..54e960b57 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResult.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResult.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. @@ -36,8 +36,6 @@ import org.xrpl.xrpl4j.model.transactions.Transaction; import org.xrpl.xrpl4j.model.transactions.TransactionMetadata; -import java.time.Instant; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Optional; @@ -82,6 +80,7 @@ static ImmutableTransactionResult.Builder builder() { * 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 @@ -118,8 +117,8 @@ default boolean validated() { Optional metadata(); /** - * The approximate close time (using Ripple Epoch) of the ledger containing this transaction. - * This is an undocumented field. + * The approximate close time (using Ripple Epoch) of the ledger containing this transaction. This is an undocumented + * field. * * @return An optionally-present {@link UnsignedLong}. */ @@ -127,8 +126,7 @@ default boolean validated() { Optional closeDate(); /** - * The approximate close time in UTC offset. - * This is derived from undocumented field. + * The approximate close time in UTC offset. This is derived from undocumented field. * * @return An optionally-present {@link ZonedDateTime}. */ diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/ObjectMapperFactory.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/ObjectMapperFactory.java index 6cd8b0594..e6fea722c 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/ObjectMapperFactory.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/ObjectMapperFactory.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. @@ -49,13 +49,20 @@ public static ObjectMapper create() { return JsonMapper.builder() .addModule(new Jdk8Module()) .addModule(new GuavaModule()) - .addModule(new Xrpl4jModule()) - .addModule(new JavaTimeModule()) .addModule(new CryptoConditionsModule(Encoding.HEX)) + // Developer Note: The ordering here is important. The JavaTimeModule must be added before the Xrpl4jModule so + // that the ZonedDateTimeSerializer is not overridden by the JavaTimeModule. + .addModule(new JavaTimeModule()) + .addModule(new Xrpl4jModule()) .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) .configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true) .configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + // Developer NOTE: Despite `serializationInclusion` being deprecated, we continue using it to for maximum + // compatibility with software using xrpl4j that uses older versions of Jackson. While newer versions of + // Jackson will define a more granular way to configure this behavior that we should actually employ at some point + // (e.g., see https://github.com/FasterXML/jackson-databind/issues/2899), we purposefully don't change + // this behavior for now to ensure maximum compatibility. .serializationInclusion(JsonInclude.Include.NON_EMPTY) .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) .build(); diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/Xrpl4jModule.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/Xrpl4jModule.java index 7d25486ed..7fb47cf8e 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/Xrpl4jModule.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/Xrpl4jModule.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. @@ -24,13 +24,13 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.model.client.common.LedgerIndex; -import org.xrpl.xrpl4j.model.client.path.BookOffersOffer; import org.xrpl.xrpl4j.model.client.serverinfo.ServerInfo; import org.xrpl.xrpl4j.model.flags.Flags; import org.xrpl.xrpl4j.model.transactions.CurrencyAmount; import org.xrpl.xrpl4j.model.transactions.Transaction; import org.xrpl.xrpl4j.model.transactions.metadata.AffectedNode; -import org.xrpl.xrpl4j.model.transactions.metadata.MetaLedgerEntryType; + +import java.time.ZonedDateTime; /** * Jackson module for the xrpl4j-model project. @@ -63,12 +63,15 @@ public Xrpl4jModule() { addDeserializer(Transaction.class, new TransactionDeserializer()); addDeserializer(ServerInfo.class, new ServerInfoDeserializer()); - + addSerializer(UnsignedByteArray.class, new UnsignedByteArraySerializer()); addDeserializer(UnsignedByteArray.class, new UnsignedByteArrayDeserializer()); addSerializer(Flags.class, new FlagsSerializer()); addDeserializer(AffectedNode.class, new AffectedNodeDeserializer()); + + addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer()); + addDeserializer(ZonedDateTime.class, new ZonedDateTimeDeserializer()); } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/ZonedDateTimeDeserializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/ZonedDateTimeDeserializer.java new file mode 100644 index 000000000..05f71dcd0 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/ZonedDateTimeDeserializer.java @@ -0,0 +1,56 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; + +import java.io.IOException; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +/** + * A custom Jackson deserializer for {@link ZonedDateTime} usage in xrpl4j. + * + *

This deserializer is needed to allow newer versions of Jackson to deserialize ZonedDateTime values that conform + * to the xrpld format, which uses `UTC` for all timezone designations. Newer versions of Jackson instead emit `Z`, so + * this serializer ensures that Jackson's serialization for ZonedDateTime uses "UTC" instead of "Z".

+ */ +public class ZonedDateTimeDeserializer extends JsonDeserializer implements ContextualDeserializer { + + private static final DateTimeFormatter DEFAULT_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSSSSS z", Locale.US); + + private final DateTimeFormatter formatter; + + public ZonedDateTimeDeserializer() { + this(DEFAULT_FORMATTER); + } + + private ZonedDateTimeDeserializer(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + @Override + public ZonedDateTime deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { + ZonedDateTime parsed = ZonedDateTime.parse(jsonParser.getText(), formatter); + // Ensure the zone is explicitly UTC, not just Z + return parsed.withZoneSameInstant(ZoneOffset.UTC); + } + + @Override + public JsonDeserializer createContextual(DeserializationContext context, BeanProperty property) { + if (property != null) { + JsonFormat.Value format = property.findPropertyFormat(context.getConfig(), ZonedDateTime.class); + if (format != null && format.hasPattern()) { + Locale locale = format.hasLocale() ? format.getLocale() : Locale.US; + return new ZonedDateTimeDeserializer(DateTimeFormatter.ofPattern(format.getPattern(), locale)); + } + } + return this; + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/ZonedDateTimeSerializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/ZonedDateTimeSerializer.java new file mode 100644 index 000000000..d2f58874f --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/ZonedDateTimeSerializer.java @@ -0,0 +1,62 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +/** + * Custom Jackson serializer for {@link ZonedDateTime} usage in xrpl4j. + * + *

This serializer is needed because newer Jackson versions changed how the `z` pattern outputs `UTC` time zones. + * Previous versions of Jackson would output UTC timezones using `UTC`. Newer versions of Jackson instead emit `Z` in + * all cases. This serializer ensures that Jackson's serialization for ZonedDateTime uses "UTC" instead of "Z" to match + * the format used by xrpld.

+ */ +public class ZonedDateTimeSerializer extends StdSerializer implements ContextualSerializer { + + private static final DateTimeFormatter DEFAULT_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSSSSS 'UTC'", Locale.US); + + private final DateTimeFormatter formatter; + + public ZonedDateTimeSerializer() { + this(DEFAULT_FORMATTER); + } + + private ZonedDateTimeSerializer(DateTimeFormatter formatter) { + super(ZonedDateTime.class); + this.formatter = formatter; + } + + @Override + public void serialize(ZonedDateTime value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + ZonedDateTime utcValue = value.withZoneSameInstant(ZoneOffset.UTC); + String formatted = utcValue.format(formatter); + gen.writeString(formatted); + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) { + if (property != null) { + JsonFormat.Value format = findFormatOverrides(prov, property, handledType()); + if (format != null && format.hasPattern()) { + // Replace the 'z' pattern with literal 'UTC' to avoid Z vs UTC issues + String pattern = format.getPattern().replace(" z", " 'UTC'"); + Locale locale = format.hasLocale() ? format.getLocale() : Locale.US; + return new ZonedDateTimeSerializer(DateTimeFormatter.ofPattern(pattern, locale)); + } + } + return this; + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Batch.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Batch.java index 90ec085d3..2fa725659 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Batch.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Batch.java @@ -155,14 +155,25 @@ default void validateNoNestedBatches() { */ @Value.Check default void validateOuterSigner() { - final Address outerSigner = account(); - for (RawTransactionWrapper wrapper : rawTransactions()) { - Preconditions.checkArgument( - !wrapper.rawTransaction().account().equals(outerSigner), - "The Account submitting a Batch transaction must not sign any inner transactions." - ); - } + final boolean anySignerMatchesOuterAccount = this.batchSigners().stream() + .map(BatchSignerWrapper::batchSigner) + .anyMatch(batchSigner -> { + // Check single-sig + if (batchSigner.account().equals(this.account())) { + return true; + } else { + // Check multi-sig + return batchSigner.signers().stream() + .anyMatch(signerWrapper -> signerWrapper.signer().account().equals(this.account())); + } + }); + + // Rule: If there is any BatchSigner then the outer signer must not be in that list, either single or multi-sig. + Preconditions.checkArgument( + !anySignerMatchesOuterAccount, + "The Account submitting a Batch transaction must not sign any inner transactions." + ); } } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/AbstractJsonTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/AbstractJsonTest.java index 6ec181716..641bd8afb 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/AbstractJsonTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/AbstractJsonTest.java @@ -23,7 +23,6 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; @@ -38,6 +37,13 @@ import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.transactions.Transaction; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Locale; +import java.util.Objects; + public class AbstractJsonTest { protected Logger logger = LoggerFactory.getLogger(this.getClass()); @@ -98,4 +104,34 @@ protected void assertCanDeserialize(String json, XrplResult result) throws JsonP XrplResult deserialized = objectMapper.readValue(json, result.getClass()); assertThat(deserialized).isEqualTo(result); } -} \ No newline at end of file + + /** + * Parses a Ripple-specific datetime string into a {@link ZonedDateTime} object. + *

+ * Xrpld uses different precision for different fields, for example: + * + *

    + *
  1. Some use 9 decimal places: 2020-Mar-24 01:41:11.000000000 UTC
  2. + *
  3. Some use 6 decimal places: 2020-Mar-24 01:27:42.147330 UTC
  4. + *
+ * + *

This method supports variable precision for the fractional seconds part. + * + * @param rippleDateTimeString The datetime string to parse; must not be null. + * + * @return A {@link ZonedDateTime} object representing the parsed datetime. + * + * @throws NullPointerException if the {@code rippleDateTimeString} is null. + * @throws DateTimeParseException if the given string cannot be parsed. + */ + protected static ZonedDateTime parseRippledTime(final String rippleDateTimeString) { + Objects.requireNonNull(rippleDateTimeString); + // Pattern handles 1-9 decimal places flexibly (see Javadoc above) + ZonedDateTime parsed = ZonedDateTime.parse( + rippleDateTimeString, + DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss[.SSSSSSSSS][.SSSSSS] z", Locale.US) + ); + // Ensure consistent UTC zone representation + return parsed.withZoneSameInstant(ZoneOffset.UTC); + } +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/accounts/AccountTransactionsTransactionTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/accounts/AccountTransactionsTransactionTest.java index 4c417db9c..c3b312f55 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/accounts/AccountTransactionsTransactionTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/accounts/AccountTransactionsTransactionTest.java @@ -11,9 +11,12 @@ import org.xrpl.xrpl4j.model.transactions.Payment; import java.time.LocalDateTime; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; +/** + * Unit tests for {@link AccountTransactionsTransaction}. + */ class AccountTransactionsTransactionTest { @Test @@ -26,8 +29,8 @@ void testCloseTimeHuman() { .build(); assertThat(payment.closeDateHuman()).hasValue( - ZonedDateTime.of(LocalDateTime.of(2021, 2, 9, 19, 1, 0), ZoneId.of("UTC")) + ZonedDateTime.of(LocalDateTime.of(2021, 2, 9, 19, 1, 0), ZoneOffset.UTC) ); } -} \ No newline at end of file +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/common/TimeUtilsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/common/TimeUtilsTest.java index e8c693501..21e593180 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/common/TimeUtilsTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/common/TimeUtilsTest.java @@ -6,15 +6,18 @@ import org.junit.jupiter.api.Test; import java.time.LocalDateTime; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; +/** + * Unit tests for {@link TimeUtils}. + */ class TimeUtilsTest { @Test void convertXrplTimeToZonedDateTime() { UnsignedLong xrplTimestamp = UnsignedLong.valueOf(666212460); ZonedDateTime zonedDateTime = TimeUtils.xrplTimeToZonedDateTime(xrplTimestamp); - assertThat(zonedDateTime).isEqualTo(ZonedDateTime.of(LocalDateTime.of(2021, 2, 9, 19, 1, 0), ZoneId.of("UTC"))); + assertThat(zonedDateTime).isEqualTo(ZonedDateTime.of(LocalDateTime.of(2021, 2, 9, 19, 1, 0), ZoneOffset.UTC)); } -} \ No newline at end of file +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerResultJsonTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerResultJsonTests.java index 3598f52d7..87d3713e3 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerResultJsonTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerResultJsonTests.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,9 +31,6 @@ import org.xrpl.xrpl4j.model.transactions.Hash256; import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.Locale; public class LedgerResultJsonTests extends AbstractJsonTest { @@ -50,9 +47,7 @@ public void testJson() throws JsonProcessingException, JSONException { LedgerHeader.builder() .accountHash(Hash256.of("B258A8BB4743FB74CBBD6E9F67E4A56C4432EA09E5805E4CC2DA26F2DBE8F3D1")) .closeTime(UnsignedLong.valueOf(638329271)) - .closeTimeHuman(ZonedDateTime.parse("2020-Mar-24 01:41:11.000000000 UTC", - DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSSSSS z", Locale.US)) - .withZoneSameLocal(ZoneId.of("UTC"))) + .closeTimeHuman(parseRippledTime("2020-Mar-24 01:41:11.000000000 UTC")) .closeTimeResolution(UnsignedInteger.valueOf(10)) .closed(true) .ledgerHash(Hash256.of("3652D7FD0576BC452C0D2E9B747BDD733075971D1A9A1D98125055DEF428721A")) @@ -86,7 +81,6 @@ public void testJson() throws JsonProcessingException, JSONException { " }"; assertCanSerializeAndDeserialize(result, json); - } @Test diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/LedgerRangeUtilsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/LedgerRangeUtilsTest.java index 09552ff49..768b8d914 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/LedgerRangeUtilsTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/LedgerRangeUtilsTest.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. @@ -46,12 +46,12 @@ public void completeLedgersRanges() { assertThat(reportingServerInfo.map( ($) -> null, ($) -> null, - param -> param.completeLedgers() + ServerInfo::completeLedgers ).size()).isEqualTo(0); ServerInfo rippledServerInfo = RippledServerInfoTest.rippledServerInfo("empty"); assertThat(rippledServerInfo.map( - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null, ($) -> null ).size()).isEqualTo(0); @@ -59,35 +59,35 @@ public void completeLedgersRanges() { serverInfo = ClioServerInfoTest.clioServerInfo(""); assertThat(serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ).size()).isEqualTo(0); serverInfo = ClioServerInfoTest.clioServerInfo("foo"); assertThat(serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ).size()).isEqualTo(0); serverInfo = ClioServerInfoTest.clioServerInfo("foo100"); assertThat(serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ).size()).isEqualTo(0); serverInfo = ClioServerInfoTest.clioServerInfo("1--2"); assertThat(serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ).size()).isEqualTo(0); serverInfo = ClioServerInfoTest.clioServerInfo("0"); List> ranges = serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ); assertThat(ranges).hasSize(1); @@ -97,7 +97,7 @@ public void completeLedgersRanges() { serverInfo = ClioServerInfoTest.clioServerInfo("1"); ranges = serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ); assertThat(ranges).hasSize(1); @@ -108,7 +108,7 @@ public void completeLedgersRanges() { serverInfo = ClioServerInfoTest.clioServerInfo("1-2"); ranges = serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ); assertThat(ranges).hasSize(1); @@ -120,7 +120,7 @@ public void completeLedgersRanges() { serverInfo = ClioServerInfoTest.clioServerInfo("0-" + UnsignedLong.MAX_VALUE.toString()); ranges = serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ); assertThat(ranges).hasSize(1); @@ -128,7 +128,7 @@ public void completeLedgersRanges() { serverInfo = ClioServerInfoTest.clioServerInfo("0-foo"); ranges = serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ); assertThat(ranges).hasSize(0); @@ -136,7 +136,7 @@ public void completeLedgersRanges() { serverInfo = ClioServerInfoTest.clioServerInfo("foo-0"); ranges = serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ); assertThat(ranges).hasSize(0); @@ -144,7 +144,7 @@ public void completeLedgersRanges() { serverInfo = ClioServerInfoTest.clioServerInfo("foo-0,bar-20"); ranges = serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ); assertThat(ranges).hasSize(0); @@ -152,7 +152,7 @@ public void completeLedgersRanges() { serverInfo = ClioServerInfoTest.clioServerInfo("0-10,20-30"); ranges = serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ); assertThat(ranges).hasSize(2); @@ -169,7 +169,7 @@ public void completeLedgersRanges() { serverInfo = ClioServerInfoTest.clioServerInfo("0-10, 20-30 "); // <-- Test the trim function ranges = serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ); assertThat(ranges).hasSize(2); @@ -186,7 +186,7 @@ public void completeLedgersRanges() { serverInfo = ClioServerInfoTest.clioServerInfo(UnsignedLong.MAX_VALUE.toString()); ranges = serverInfo.map( ($) -> null, - param -> param.completeLedgers(), + ServerInfo::completeLedgers, ($) -> null ); assertThat(ranges).hasSize(1); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ReportingModeServerInfoTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ReportingModeServerInfoTest.java index 7b73f580b..fda222956 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ReportingModeServerInfoTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ReportingModeServerInfoTest.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. @@ -36,9 +36,6 @@ import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; import java.math.BigDecimal; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Objects; @@ -265,8 +262,7 @@ public void deserializeActualReportingModeResponse() throws JsonProcessingExcept .publicKeyNode("n9M6hbCsX1wudMmDjQw1LpXRXvsC1oKSGPizH18X11TF9AzL2cFg") .serverState("full") .serverStateDurationUs("5458855314538") - .time(ZonedDateTime.parse("2022-Jul-26 19:14:43.183314 UTC", - DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSS z", Locale.US)).withZoneSameLocal(ZoneId.of("UTC"))) + .time(parseRippledTime("2022-Jul-26 19:14:43.183314 UTC")) .upTime(UnsignedLong.valueOf(5458859)) .validatedLedger(ValidatedLedger.builder() .age(UnsignedInteger.valueOf(3)) @@ -356,8 +352,7 @@ protected static ServerInfo reportingServerInfo(final String completeLedgers) { .publicKeyValidator("nHBk5DPexBjinXV8qHn7SEKzoxh2W92FxSbNTPgGtQYBzEF4msn9") .serverState("proposing") .serverStateDurationUs("1850969666") - .time(ZonedDateTime.parse("2020-Mar-24 01:27:42.147330 UTC", - DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSS z", Locale.US)).withZoneSameLocal(ZoneId.of("UTC"))) + .time(parseRippledTime("2020-Mar-24 01:27:42.147330 UTC")) .upTime(UnsignedLong.valueOf(1984)) .validatedLedger(ValidatedLedger.builder() .age(UnsignedInteger.valueOf(2)) diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/RippledServerInfoTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/RippledServerInfoTest.java index 5da272baf..4cad69d96 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/RippledServerInfoTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/RippledServerInfoTest.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. @@ -36,9 +36,6 @@ import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; import java.math.BigDecimal; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Objects; @@ -226,8 +223,7 @@ public void deserializeActualRippledResponse() throws JsonProcessingException { .publicKeyNode("n9MmdUoYxpTMPhD8Fxky48wXnmr3zu5hqG1httdLrD8JY66GbdTq") .serverState("full") .serverStateDurationUs("20369650238475") - .time(ZonedDateTime.parse("2022-Jul-26 19:14:02.337455 UTC", - DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSS z", Locale.US)).withZoneSameLocal(ZoneId.of("UTC"))) + .time(parseRippledTime("2022-Jul-26 19:14:02.337455 UTC")) .upTime(UnsignedLong.valueOf(20369659)) .validatedLedger(ValidatedLedger.builder() .age(UnsignedInteger.ONE) @@ -321,8 +317,7 @@ protected static ServerInfo rippledServerInfo(final String completeLedgers) { .publicKeyValidator("nHBk5DPexBjinXV8qHn7SEKzoxh2W92FxSbNTPgGtQYBzEF4msn9") .serverState("proposing") .serverStateDurationUs("1850969666") - .time(ZonedDateTime.parse("2020-Mar-24 01:27:42.147330 UTC", - DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSS z", Locale.US)).withZoneSameLocal(ZoneId.of("UTC"))) + .time(parseRippledTime("2020-Mar-24 01:27:42.147330 UTC")) .upTime(UnsignedLong.valueOf(1984)) .validatedLedger(ValidatedLedger.builder() .age(UnsignedInteger.valueOf(2)) diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ServerInfoResultTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ServerInfoResultTest.java index f7a1da78f..2cd277a35 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ServerInfoResultTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ServerInfoResultTest.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. @@ -25,7 +25,6 @@ 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.Assertions; import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.model.AbstractJsonTest; @@ -37,10 +36,6 @@ import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; import java.math.BigDecimal; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Locale; /** * Unit tests for {@link ServerInfoResult}. @@ -100,8 +95,7 @@ void testJsonWithNetworkId() throws JsonProcessingException { .publicKeyNode("n9MozjnGB3tpULewtTsVtuudg5JqYFyV3QFdAtVLzJaxHcBaxuXD") .serverState("full") .serverStateDurationUs("2274468435925") - .time(ZonedDateTime.parse("2021-Mar-30 15:37:51.486384 UTC", - DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSS z", Locale.US)).withZoneSameLocal(ZoneId.of("UTC"))) + .time(parseRippledTime("2021-Mar-30 15:37:51.486384 UTC")) .upTime(UnsignedLong.valueOf(2274704)) .validatedLedger(ValidatedLedger.builder() .age(UnsignedInteger.valueOf(4)) @@ -174,8 +168,7 @@ void testJsonWithoutNetworkId() throws JsonProcessingException { .publicKeyNode("n9MozjnGB3tpULewtTsVtuudg5JqYFyV3QFdAtVLzJaxHcBaxuXD") .serverState("full") .serverStateDurationUs("2274468435925") - .time(ZonedDateTime.parse("2021-Mar-30 15:37:51.486384 UTC", - DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSS z", Locale.US)).withZoneSameLocal(ZoneId.of("UTC"))) + .time(parseRippledTime("2021-Mar-30 15:37:51.486384 UTC")) .upTime(UnsignedLong.valueOf(2274704)) .validatedLedger(ValidatedLedger.builder() .age(UnsignedInteger.valueOf(4)) diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ServerInfoTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ServerInfoTests.java index e5e49f0a3..435e44c36 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ServerInfoTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/serverinfo/ServerInfoTests.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. @@ -46,7 +46,7 @@ public void setUp() { @Test void map() { String rippledVersion = rippledServerInfo.map( - rippledServerInfoCopy -> rippledServerInfoCopy.buildVersion(), + RippledServerInfo::buildVersion, ($) -> fail(), ($) -> fail() ); @@ -54,7 +54,7 @@ void map() { String clioVersion = clioServerInfo.map( ($) -> fail(), - clioServerInfoCopy -> clioServerInfoCopy.clioVersion(), + ClioServerInfo::clioVersion, ($) -> fail() ); assertThat(clioVersion).isEqualTo("1.5.0-rc1"); @@ -62,7 +62,7 @@ void map() { String reportingModeVersion = reportingServerInfo.map( ($) -> fail(), ($) -> fail(), - reportingModeServerInfo -> reportingModeServerInfo.buildVersion() + ReportingModeServerInfo::buildVersion ); assertThat(reportingModeVersion).isEqualTo("1.5.0-rc1"); } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResultJsonTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResultJsonTests.java index dd49f4ebc..da1131fdd 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResultJsonTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResultJsonTests.java @@ -40,7 +40,7 @@ import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; import java.time.LocalDateTime; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; public class TransactionResultJsonTests extends AbstractJsonTest { @@ -76,7 +76,7 @@ public void testPaymentTransactionResultJson() throws JsonProcessingException, J .build(); assertThat(paymentResult.closeDateHuman()).hasValue( - ZonedDateTime.of(LocalDateTime.of(2021, 2, 9, 19, 1, 0), ZoneId.of("UTC")) + ZonedDateTime.of(LocalDateTime.of(2021, 2, 9, 19, 1, 0), ZoneOffset.UTC) ); String json = "{\n" + diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResultTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResultTest.java index f03b964e3..6a0cb3706 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResultTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResultTest.java @@ -33,7 +33,7 @@ import org.xrpl.xrpl4j.model.transactions.TransactionMetadata; import java.time.LocalDateTime; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; class TransactionResultTest { @@ -77,7 +77,7 @@ void testCloseTimeHuman() { .build(); assertThat(paymentResult.closeDateHuman()).hasValue( - ZonedDateTime.of(LocalDateTime.of(2021, 2, 9, 19, 1, 0), ZoneId.of("UTC")) + ZonedDateTime.of(LocalDateTime.of(2021, 2, 9, 19, 1, 0), ZoneOffset.UTC) ); } } 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 deleted file mode 100644 index 78603d3c9..000000000 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AccountRootFlagsTests.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.xrpl.xrpl4j.model.flags; - -/*- - * ========================LICENSE_START================================= - * xrpl4j :: model - * %% - * Copyright (C) 2020 - 2022 XRPL Foundation and its contributors - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * =========================LICENSE_END================================== - */ - -import static org.assertj.core.api.Assertions.assertThat; - -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 java.util.stream.Stream; - -public class AccountRootFlagsTests extends AbstractFlagsTest { - - public static Stream data() { - return getBooleanCombinations(15); - } - - @ParameterizedTest - @MethodSource("data") - @SuppressWarnings("AbbreviationAsWordInName") - public void testDeriveIndividualFlagsFromFlags( - boolean lsfDefaultRipple, - boolean lsfDepositAuth, - boolean lsfDisableMaster, - boolean lsfDisallowXrp, - boolean lsfGlobalFreeze, - boolean lsfNoFreeze, - boolean lsfPasswordSpent, - boolean lsfRequireAuth, - boolean lsfRequireDestTag, - boolean lsfDisallowIncomingNFTokenOffer, - boolean lsfDisallowIncomingCheck, - boolean lsfDisallowIncomingPayChan, - boolean lsfDisallowIncomingTrustline, - boolean lsfAllowTrustlineClawback, - boolean lsfAllowTrustlineLocking - ) { - long expectedFlags = (lsfDefaultRipple ? AccountRootFlags.DEFAULT_RIPPLE.getValue() : 0L) | - (lsfDepositAuth ? AccountRootFlags.DEPOSIT_AUTH.getValue() : 0L) | - (lsfDisableMaster ? AccountRootFlags.DISABLE_MASTER.getValue() : 0L) | - (lsfDisallowXrp ? AccountRootFlags.DISALLOW_XRP.getValue() : 0L) | - (lsfGlobalFreeze ? AccountRootFlags.GLOBAL_FREEZE.getValue() : 0L) | - (lsfNoFreeze ? AccountRootFlags.NO_FREEZE.getValue() : 0L) | - (lsfPasswordSpent ? AccountRootFlags.PASSWORD_SPENT.getValue() : 0L) | - (lsfRequireAuth ? AccountRootFlags.REQUIRE_AUTH.getValue() : 0L) | - (lsfRequireDestTag ? AccountRootFlags.REQUIRE_DEST_TAG.getValue() : 0L) | - (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) | - (lsfAllowTrustlineClawback ? AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK.getValue() : 0L) | - (lsfAllowTrustlineLocking ? AccountRootFlags.ALLOW_TRUSTLINE_LOCKING.getValue() : 0L); - Flags flagsFromFlags = AccountRootFlags.of( - (lsfDefaultRipple ? AccountRootFlags.DEFAULT_RIPPLE : AccountRootFlags.UNSET), - (lsfDepositAuth ? AccountRootFlags.DEPOSIT_AUTH : AccountRootFlags.UNSET), - (lsfDisableMaster ? AccountRootFlags.DISABLE_MASTER : AccountRootFlags.UNSET), - (lsfDisallowXrp ? AccountRootFlags.DISALLOW_XRP : AccountRootFlags.UNSET), - (lsfGlobalFreeze ? AccountRootFlags.GLOBAL_FREEZE : AccountRootFlags.UNSET), - (lsfNoFreeze ? AccountRootFlags.NO_FREEZE : AccountRootFlags.UNSET), - (lsfPasswordSpent ? AccountRootFlags.PASSWORD_SPENT : AccountRootFlags.UNSET), - (lsfRequireAuth ? AccountRootFlags.REQUIRE_AUTH : AccountRootFlags.UNSET), - (lsfRequireDestTag ? AccountRootFlags.REQUIRE_DEST_TAG : AccountRootFlags.UNSET), - (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), - (lsfAllowTrustlineClawback ? AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK : AccountRootFlags.UNSET), - (lsfAllowTrustlineLocking ? AccountRootFlags.ALLOW_TRUSTLINE_LOCKING : AccountRootFlags.UNSET) - ); - assertThat(flagsFromFlags.getValue()).isEqualTo(expectedFlags); - - AccountRootFlags flagsFromLong = AccountRootFlags.of(expectedFlags); - - assertThat(flagsFromLong.getValue()).isEqualTo(expectedFlags); - - assertThat(flagsFromLong.lsfDefaultRipple()).isEqualTo(lsfDefaultRipple); - assertThat(flagsFromLong.lsfDepositAuth()).isEqualTo(lsfDepositAuth); - assertThat(flagsFromLong.lsfDisableMaster()).isEqualTo(lsfDisableMaster); - assertThat(flagsFromLong.lsfDisallowXrp()).isEqualTo(lsfDisallowXrp); - assertThat(flagsFromLong.lsfGlobalFreeze()).isEqualTo(lsfGlobalFreeze); - assertThat(flagsFromLong.lsfNoFreeze()).isEqualTo(lsfNoFreeze); - assertThat(flagsFromLong.lsfPasswordSpent()).isEqualTo(lsfPasswordSpent); - assertThat(flagsFromLong.lsfRequireAuth()).isEqualTo(lsfRequireAuth); - assertThat(flagsFromLong.lsfRequireDestTag()).isEqualTo(lsfRequireDestTag); - assertThat(flagsFromLong.lsfDisallowIncomingNFTokenOffer()).isEqualTo(lsfDisallowIncomingNFTokenOffer); - assertThat(flagsFromLong.lsfDisallowIncomingCheck()).isEqualTo(lsfDisallowIncomingCheck); - assertThat(flagsFromLong.lsfDisallowIncomingPayChan()).isEqualTo(lsfDisallowIncomingPayChan); - assertThat(flagsFromLong.lsfDisallowIncomingTrustline()).isEqualTo(lsfDisallowIncomingTrustline); - assertThat(flagsFromLong.lsfAllowTrustLineClawback()).isEqualTo(lsfAllowTrustlineClawback); - assertThat(flagsFromLong.lsfAllowTrustLineLocking()).isEqualTo(lsfAllowTrustlineLocking); - } - - @ParameterizedTest - @MethodSource("data") - @SuppressWarnings("AbbreviationAsWordInName") - void testJson( - boolean lsfDefaultRipple, - boolean lsfDepositAuth, - boolean lsfDisableMaster, - boolean lsfDisallowXrp, - boolean lsfGlobalFreeze, - boolean lsfNoFreeze, - boolean lsfPasswordSpent, - boolean lsfRequireAuth, - boolean lsfRequireDestTag, - boolean lsfDisallowIncomingNFTokenOffer, - boolean lsfDisallowIncomingCheck, - boolean lsfDisallowIncomingPayChan, - boolean lsfDisallowIncomingTrustline, - boolean lsfAllowTrustlineClawback, - boolean lsfAllowTrustlineLocking - ) throws JSONException, JsonProcessingException { - Flags flags = AccountRootFlags.of( - (lsfDefaultRipple ? AccountRootFlags.DEFAULT_RIPPLE : AccountRootFlags.UNSET), - (lsfDepositAuth ? AccountRootFlags.DEPOSIT_AUTH : AccountRootFlags.UNSET), - (lsfDisableMaster ? AccountRootFlags.DISABLE_MASTER : AccountRootFlags.UNSET), - (lsfDisallowXrp ? AccountRootFlags.DISALLOW_XRP : AccountRootFlags.UNSET), - (lsfGlobalFreeze ? AccountRootFlags.GLOBAL_FREEZE : AccountRootFlags.UNSET), - (lsfNoFreeze ? AccountRootFlags.NO_FREEZE : AccountRootFlags.UNSET), - (lsfPasswordSpent ? AccountRootFlags.PASSWORD_SPENT : AccountRootFlags.UNSET), - (lsfRequireAuth ? AccountRootFlags.REQUIRE_AUTH : AccountRootFlags.UNSET), - (lsfRequireDestTag ? AccountRootFlags.REQUIRE_DEST_TAG : AccountRootFlags.UNSET), - (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), - (lsfAllowTrustlineClawback ? AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK : AccountRootFlags.UNSET), - (lsfAllowTrustlineLocking ? AccountRootFlags.ALLOW_TRUSTLINE_LOCKING : AccountRootFlags.UNSET) - ); - - FlagsWrapper flagsWrapper = FlagsWrapper.of(flags); - - String json = String.format("{\n" + - " \"flags\": %s\n" + - "}", flags.getValue()); - - assertCanSerializeAndDeserialize(flagsWrapper, json); - } -} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/LedgerHeaderJsonTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/LedgerHeaderJsonTests.java index 2a7de507c..8f419ebba 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/LedgerHeaderJsonTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/LedgerHeaderJsonTests.java @@ -42,11 +42,6 @@ import org.xrpl.xrpl4j.model.transactions.Payment; import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Locale; - public class LedgerHeaderJsonTests extends AbstractJsonTest { @Test @@ -54,8 +49,7 @@ public void deserializeLedgerHeaderWithTransactions() throws JsonProcessingExcep LedgerHeader ledgerHeader = LedgerHeader.builder() .accountHash(Hash256.of("B258A8BB4743FB74CBBD6E9F67E4A56C4432EA09E5805E4CC2DA26F2DBE8F3D1")) .closeTime(UnsignedLong.valueOf(638329271)) - .closeTimeHuman(ZonedDateTime.parse("2020-Mar-24 01:41:11.000000000 UTC", - DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSSSSS z", Locale.US)).withZoneSameLocal(ZoneId.of("UTC"))) + .closeTimeHuman(parseRippledTime("2020-Mar-24 01:41:11.000000000 UTC")) .closeTimeResolution(UnsignedInteger.valueOf(10)) .closed(true) .ledgerHash(Hash256.of("3652D7FD0576BC452C0D2E9B747BDD733075971D1A9A1D98125055DEF428721A")) @@ -175,8 +169,7 @@ public void deserializeWithProblematicTimeStamp() throws JsonProcessingException LedgerHeader ledgerHeader = LedgerHeader.builder() .accountHash(Hash256.of("B258A8BB4743FB74CBBD6E9F67E4A56C4432EA09E5805E4CC2DA26F2DBE8F3D1")) .closeTime(UnsignedLong.valueOf(638329271)) - .closeTimeHuman(ZonedDateTime.parse("2021-Jun-11 09:06:10.000000000 UTC", - DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss.SSSSSSSSS z", Locale.US)).withZoneSameLocal(ZoneId.of("UTC"))) + .closeTimeHuman(parseRippledTime("2021-Jun-11 09:06:10.000000000 UTC")) .closeTimeResolution(UnsignedInteger.valueOf(10)) .closed(true) .ledgerHash(Hash256.of("3652D7FD0576BC452C0D2E9B747BDD733075971D1A9A1D98125055DEF428721A")) diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/BatchTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/BatchTest.java index a3faa4bbd..ef31f3b19 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/BatchTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/BatchTest.java @@ -23,8 +23,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.google.common.collect.Lists; import com.google.common.primitives.UnsignedInteger; import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; import org.xrpl.xrpl4j.crypto.keys.Seed; import org.xrpl.xrpl4j.crypto.signing.Signature; import org.xrpl.xrpl4j.model.flags.BatchFlags; @@ -249,6 +251,40 @@ void testBatchWithRawTransactionSignedBySubmitterAccount() { .sequence(UnsignedInteger.ONE) .flags(BatchFlags.ALL_OR_NOTHING) .rawTransactions(innerTransactions) + .batchSigners(Lists.newArrayList( + BatchSignerWrapper.of(BatchSigner.builder() + .account(innerTransactions.get(0).rawTransaction().account()) + .signingPublicKey(innerTransactions.get(0).rawTransaction().signingPublicKey()) + .transactionSignature(Signature.fromBase16("ABCD")) + .build() + ))) + .build() + ).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("The Account submitting a Batch transaction must not sign any inner transactions."); + } + + @Test + void testBatchWithRawTransactionMultiSignedBySubmitterAccount() { + final List innerTransactions = createInnerTransactions(2); + assertThatThrownBy(() -> Batch.builder() + .account(innerTransactions.get(0).rawTransaction().account()) // <-- The crux of the test + .fee(XrpCurrencyAmount.ofDrops(100)) + .sequence(UnsignedInteger.ONE) + .flags(BatchFlags.ALL_OR_NOTHING) + .rawTransactions(innerTransactions) + .batchSigners(Lists.newArrayList( + BatchSignerWrapper.of( + BatchSigner.builder() + .account(innerTransactions.get(1).rawTransaction().account()) // <-- The crux of the test + .signers(Lists.newArrayList( + SignerWrapper.of(Signer.builder() + .account(innerTransactions.get(0).rawTransaction().account()) // <-- The crux of the test + .signingPublicKey(innerTransactions.get(0).rawTransaction().signingPublicKey()) + .transactionSignature(Signature.fromBase16("ABCD")) + .build() + ) + )).build() + ))) .build() ).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("The Account submitting a Batch transaction must not sign any inner transactions."); @@ -282,7 +318,7 @@ private Batch createValidBatch(BatchFlags batchFlags) { .build(); } - private Payment innterTransaction(UnsignedInteger sequence) { + private Payment innerTransaction(UnsignedInteger sequence) { return Payment.builder() .account(Seed.ed25519Seed().deriveKeyPair().publicKey().deriveAddress()) .destination(DESTINATION) @@ -296,7 +332,7 @@ private Payment innterTransaction(UnsignedInteger sequence) { private List createInnerTransactions(int count) { return IntStream.range(0, count) .mapToObj(i -> RawTransactionWrapper.of( - innterTransaction(UnsignedInteger.valueOf(i + 1)) + innerTransaction(UnsignedInteger.valueOf(i + 1)) )) .collect(Collectors.toList()); } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/BatchTransactionIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/BatchTransactionIT.java new file mode 100644 index 000000000..5e1071758 --- /dev/null +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/BatchTransactionIT.java @@ -0,0 +1,1286 @@ +package org.xrpl.xrpl4j.tests; + +/*- + * ========================LICENSE_START================================= + * xrpl4j :: integration-tests + * %% + * Copyright (C) 2020 - 2026 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 static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.assertj.core.util.Lists; +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.PublicKey; +import org.xrpl.xrpl4j.crypto.signing.MultiSignedTransaction; +import org.xrpl.xrpl4j.crypto.signing.Signature; +import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; +import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.fees.FeeResult; +import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +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.flags.BatchFlags; +import org.xrpl.xrpl4j.model.flags.PaymentFlags; +import org.xrpl.xrpl4j.model.ledger.SignerEntry; +import org.xrpl.xrpl4j.model.ledger.SignerEntryWrapper; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Batch; +import org.xrpl.xrpl4j.model.transactions.BatchSigner; +import org.xrpl.xrpl4j.model.transactions.BatchSignerWrapper; +import org.xrpl.xrpl4j.model.transactions.Payment; +import org.xrpl.xrpl4j.model.transactions.RawTransactionWrapper; +import org.xrpl.xrpl4j.model.transactions.Signer; +import org.xrpl.xrpl4j.model.transactions.SignerListSet; +import org.xrpl.xrpl4j.model.transactions.SignerWrapper; +import org.xrpl.xrpl4j.model.transactions.TransactionMetadata; +import org.xrpl.xrpl4j.model.transactions.TransactionResultCodes; +import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Integration tests for {@link Batch} transactions (XLS-0056). + */ +@DisabledIf(value = "shouldNotRun", disabledReason = "BatchTransactionIT only runs on local rippled node.") +public class BatchTransactionIT extends AbstractIT { + + static boolean shouldNotRun() { + return System.getProperty("useTestnet") != null || + System.getProperty("useClioTestnet") != null; + } + + // ////////////////////// + // 1. This section tests a Batch transaction with two inner payments where all transactions are signed + // the same single-sign account. Each Batch flag is tested over the same test. + // ////////////////////// + + /** + * Test a single-account batch transaction containing two payments. + */ + @Test + void batchWithInnerSingleSigOuterSingleSigSame() throws JsonRpcClientErrorException, JsonProcessingException { + final KeyPair sourceKeyPair = createRandomAccountEd25519(); + final KeyPair destination1KeyPair = createRandomAccountEd25519(); + final KeyPair destination2KeyPair = createRandomAccountEd25519(); + + batchWithInnerSingleSigOuterSingleSigSameHelper(sourceKeyPair, destination1KeyPair, destination2KeyPair, + BatchFlags.ofAllOrNothing()); + batchWithInnerSingleSigOuterSingleSigSameHelper(sourceKeyPair, destination1KeyPair, destination2KeyPair, + BatchFlags.ofOnlyOne()); + batchWithInnerSingleSigOuterSingleSigSameHelper(sourceKeyPair, destination1KeyPair, destination2KeyPair, + BatchFlags.ofUntilFailure()); + batchWithInnerSingleSigOuterSingleSigSameHelper(sourceKeyPair, destination1KeyPair, destination2KeyPair, + BatchFlags.ofIndependent()); + } + + /** + * Helper method to Test a single-account batch transaction containing two payments with the specified Batch flags. + */ + private void batchWithInnerSingleSigOuterSingleSigSameHelper( + final KeyPair sourceKeyPair, + final KeyPair destination1KeyPair, + final KeyPair destination2KeyPair, + final BatchFlags batchFlags + ) throws JsonRpcClientErrorException, JsonProcessingException { + Objects.requireNonNull(sourceKeyPair); + Objects.requireNonNull(destination1KeyPair); + Objects.requireNonNull(destination2KeyPair); + Objects.requireNonNull(batchFlags); + + FeeResult feeResult = xrplClient.fee(); + AccountInfoResult sourceAccountInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(sourceKeyPair.publicKey().deriveAddress()) + ); + + // Create inner payment1 + Payment innerPayment1 = createInnerPayment( + sourceKeyPair.publicKey().deriveAddress(), + sourceAccountInfo.accountData().sequence().plus(UnsignedInteger.ONE), + destination1KeyPair.publicKey().deriveAddress(), + 10000 + ); + + // Create inner payment2 + Payment innerPayment2 = createInnerPayment( + sourceKeyPair.publicKey().deriveAddress(), + sourceAccountInfo.accountData().sequence().plus(UnsignedInteger.valueOf(2L)), + destination2KeyPair.publicKey().deriveAddress(), + 20000 + ); + + // Build the Batch transaction + Batch batch = createBatchTransaction( + sourceKeyPair, feeResult, sourceAccountInfo.accountData().sequence(), + innerPayment1, innerPayment2, batchFlags + ); + + // /////////////// + // Outer Sign + // /////////////// + + // Only outer needs to be signed because the same accounts authorize inner and outer transactions. + SingleSignedTransaction signedBatch = signatureService.sign(sourceKeyPair.privateKey(), batch); + SubmitResult submitResult = xrplClient.submit(signedBatch); + assertTesSuccess(submitResult); + + TransactionResult validatedBatch = this.scanForResult( + () -> this.getValidatedTransaction(submitResult.transactionResult().hash(), Batch.class) + ); + assertTesSuccess(validatedBatch); + logger.info("Batch transaction (flags: {}) successful: {}", batchFlags, submitResult.transactionResult().hash()); + + // Verify metadata + verifyBatchMetadata(validatedBatch); + + // Verify destination accounts received the payments + AccountInfoResult dest1Info = this.scanForResult( + () -> this.getValidatedAccountInfo(destination1KeyPair.publicKey().deriveAddress()) + ); + AccountInfoResult dest2Info = this.scanForResult( + () -> this.getValidatedAccountInfo(destination2KeyPair.publicKey().deriveAddress()) + ); + + // Accounts should exist and have received funds (minus reserve) + assertThat(dest1Info.accountData().balance()).isNotNull(); + assertThat(dest2Info.accountData().balance()).isNotNull(); + } + + // ////////////////////// + // 2. This section tests a Batch transaction with two inner payments where each inner payment transaction is + // authorized by a different single-sign account. Each Batch flag is tested over the same test. + // ////////////////////// + + /** + * Test a multi-account batch transaction where multiple accounts contribute inner transactions. + */ + @Test + void batchWithInnerSingleSigOuterSingleSigDifferent() throws JsonRpcClientErrorException, JsonProcessingException { + final KeyPair account1KeyPair = createRandomAccountEd25519(); + final KeyPair account2KeyPair = createRandomAccountEd25519(); + final KeyPair destinationKeyPair = createRandomAccountEd25519(); + + batchWithInnerSingleSigOuterSingleSigDifferentHelper(account1KeyPair, account2KeyPair, destinationKeyPair, + BatchFlags.ofAllOrNothing()); + batchWithInnerSingleSigOuterSingleSigDifferentHelper(account1KeyPair, account2KeyPair, destinationKeyPair, + BatchFlags.ofOnlyOne()); + batchWithInnerSingleSigOuterSingleSigDifferentHelper(account1KeyPair, account2KeyPair, destinationKeyPair, + BatchFlags.ofUntilFailure()); + batchWithInnerSingleSigOuterSingleSigDifferentHelper(account1KeyPair, account2KeyPair, destinationKeyPair, + BatchFlags.ofIndependent()); + } + + /** + * Helper to test a multi-account batch transaction where multiple different accounts contribute inner transactions + * with the specified flags. + */ + private void batchWithInnerSingleSigOuterSingleSigDifferentHelper( + final KeyPair account1KeyPair, + final KeyPair account2KeyPair, + final KeyPair destinationKeyPair, + final BatchFlags batchFlags + ) throws JsonRpcClientErrorException, JsonProcessingException { + Objects.requireNonNull(account1KeyPair); + Objects.requireNonNull(account2KeyPair); + Objects.requireNonNull(destinationKeyPair); + Objects.requireNonNull(batchFlags); + + final FeeResult feeResult = xrplClient.fee(); + final AccountInfoResult account1Info = this.scanForResult( + () -> this.getValidatedAccountInfo(account1KeyPair.publicKey().deriveAddress()) + ); + final AccountInfoResult account2Info = this.scanForResult( + () -> this.getValidatedAccountInfo(account2KeyPair.publicKey().deriveAddress()) + ); + + // Create inner payment from account1 + final Payment innerPayment1 = createInnerPayment( + account1KeyPair.publicKey().deriveAddress(), + account1Info.accountData().sequence().plus(UnsignedInteger.ONE), + destinationKeyPair.publicKey().deriveAddress(), + 10000 + ); + + // Create inner payment from account2 + final Payment innerPayment2 = createInnerPayment( + account2KeyPair.publicKey().deriveAddress(), + account2Info.accountData().sequence(), + destinationKeyPair.publicKey().deriveAddress(), + 20000 + ); + + // Build the Batch transaction - account1 is the batch submitter + final Batch unsignedBatch = Batch.builder() + .account(account1KeyPair.publicKey().deriveAddress()) + .signingPublicKey(account1KeyPair.publicKey()) + .fee(FeeUtils.computeBatchFee(feeResult, UnsignedInteger.valueOf(2L))) + .sequence(account1Info.accountData().sequence()) + .flags(batchFlags) + .addRawTransactions( + RawTransactionWrapper.of(innerPayment1), + RawTransactionWrapper.of(innerPayment2) + ) + .build(); + + // /////////////// + // Inner Sign (account2 is the inner signer) + // /////////////// + final List signerWrappers = Lists.newArrayList( + BatchSignerWrapper.of(BatchSigner.builder() + .account(account2KeyPair.publicKey().deriveAddress()) + .signingPublicKey(account2KeyPair.publicKey()) + .transactionSignature( + signatureService.signInner(account2KeyPair.privateKey(), unsignedBatch) // <-- `signInner` is crucial here + ) + .build() + ) + ); + + // /////////////// + // Outer Sign (account1 is the batch submitter) + // /////////////// + final SingleSignedTransaction signedBatch = signatureService.sign( + account1KeyPair.privateKey(), + Batch.builder().from(unsignedBatch) + .batchSigners(signerWrappers) + .build() + ); + + // Submit and wait for validation + final SubmitResult result = xrplClient.submit(signedBatch); + assertTesSuccess(result); + final TransactionResult validatedBatch = this.scanForResult( + () -> this.getValidatedTransaction(result.transactionResult().hash(), Batch.class) + ); + assertTesSuccess(validatedBatch); + + // Verify metadata + verifyBatchMetadata(validatedBatch); + + // Verify the destination account received both payments + final AccountInfoResult destInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(destinationKeyPair.publicKey().deriveAddress()) + ); + assertThat(destInfo.accountData().balance()).isNotNull(); + } + + // ////////////////////// + // 3. This section tests a Batch transaction with two inner payments where each inner payment transaction is + // authorized by the same multi-sig account. Each Batch flag is tested over the same test. + // ////////////////////// + + /** + * Test a batch transaction where both inner transactions are from the same multi-sig account. + */ + @Test + void batchWithInnerMultiSigOuterMultiSigSame() throws JsonRpcClientErrorException, JsonProcessingException { + // Create accounts: one multi-sig account owner, two signers (signer1 and signer2), and two destinations + final KeyPair account1KeyPair = createRandomAccountEd25519(); + final KeyPair account1Signer1KeyPair = createRandomAccountEd25519(); + final KeyPair account2Signer1KeyPair = createRandomAccountEd25519(); + final KeyPair destination1KeyPair = createRandomAccountEd25519(); + final KeyPair destination2KeyPair = createRandomAccountEd25519(); + + batchWithInnerMultiSigOuterMultiSigSameHelper( + account1KeyPair, account1Signer1KeyPair, account2Signer1KeyPair, destination1KeyPair, destination2KeyPair, + BatchFlags.ofAllOrNothing() + ); + batchWithInnerMultiSigOuterMultiSigSameHelper( + account1KeyPair, account1Signer1KeyPair, account2Signer1KeyPair, destination1KeyPair, destination2KeyPair, + BatchFlags.ofOnlyOne() + ); + batchWithInnerMultiSigOuterMultiSigSameHelper( + account1KeyPair, account1Signer1KeyPair, account2Signer1KeyPair, destination1KeyPair, destination2KeyPair, + BatchFlags.ofIndependent() + ); + batchWithInnerMultiSigOuterMultiSigSameHelper( + account1KeyPair, account1Signer1KeyPair, account2Signer1KeyPair, destination1KeyPair, destination2KeyPair, + BatchFlags.ofUntilFailure() + ); + } + + /** + * Helper to a batch transaction where both inner transactions are from the same multi-sig account with the specified + * flags. + */ + private void batchWithInnerMultiSigOuterMultiSigSameHelper( + final KeyPair account1KeyPair, + final KeyPair account1Signer1KeyPair, + final KeyPair account1Signer2KeyPair, + final KeyPair destination1KeyPair, + final KeyPair destination2KeyPair, + final BatchFlags batchFlags + ) throws JsonRpcClientErrorException, JsonProcessingException { + Objects.requireNonNull(account1KeyPair); + Objects.requireNonNull(account1Signer1KeyPair); + Objects.requireNonNull(account1Signer2KeyPair); + Objects.requireNonNull(destination1KeyPair); + Objects.requireNonNull(destination2KeyPair); + Objects.requireNonNull(batchFlags); + + FeeResult feeResult = xrplClient.fee(); + + final AccountInfoResult account1Result = this.setupMultiSigAccount( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPair + ); + + final Payment innerPayment1 = createInnerPayment( + account1KeyPair.publicKey().deriveAddress(), + account1Result.accountData().sequence().plus(UnsignedInteger.ONE), + destination1KeyPair.publicKey().deriveAddress(), + 10000 + ); + // Create inner payment from account2 + final Payment innerPayment2 = createInnerPayment( + account1KeyPair.publicKey().deriveAddress(), + account1Result.accountData().sequence().plus(UnsignedInteger.valueOf(2)), + destination2KeyPair.publicKey().deriveAddress(), + 20000 + ); + + // Build the Batch transaction with Independent mode + Batch unsignedBatch = Batch.builder() + .account(account1KeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeBatchFee(feeResult, UnsignedInteger.valueOf(2L))) + .sequence(account1Result.accountData().sequence()) + .flags(batchFlags) + .addRawTransactions( + RawTransactionWrapper.of(innerPayment1), + RawTransactionWrapper.of(innerPayment2) + ) + .build(); + + // /////////////// + // Outer MultiSign (account1Signer1 and account1Signer2 are the outer multi-signers) + // /////////////// + + Set signers = Lists.newArrayList(account1Signer1KeyPair, account1Signer2KeyPair).stream() + .map(keyPair -> { + Signature signature = signatureService.multiSign(keyPair.privateKey(), unsignedBatch); + return Signer.builder() + .signingPublicKey(keyPair.publicKey()) + .transactionSignature(signature) + .build(); + }) + .collect(Collectors.toSet()); + + MultiSignedTransaction multiSignedBatch = MultiSignedTransaction.builder() + .unsignedTransaction(unsignedBatch) + .signerSet(signers) + .build(); + + // Submit and wait for validation + SubmitMultiSignedResult result = xrplClient.submitMultisigned(multiSignedBatch); + assertTesSuccess(result); + TransactionResult validatedBatch = this.scanForResult( + () -> this.getValidatedTransaction(result.transaction().hash(), Batch.class) + ); + assertTesSuccess(validatedBatch); + + // Verify metadata + verifyBatchMetadata(validatedBatch); + + // Verify destination accounts received the payments + AccountInfoResult dest1Info = this.scanForResult( + () -> this.getValidatedAccountInfo(destination1KeyPair.publicKey().deriveAddress()) + ); + AccountInfoResult dest2Info = this.scanForResult( + () -> this.getValidatedAccountInfo(destination2KeyPair.publicKey().deriveAddress()) + ); + + assertThat(dest1Info.accountData().balance()).isNotNull(); + assertThat(dest2Info.accountData().balance()).isNotNull(); + } + + // ////////////////////// + // 4. This section tests a Batch transaction with two inner payments where each inner payment transaction is + // authorized by a different multi-sig account. Each Batch flag is tested over the same test. + // ////////////////////// + + /** + * Test a batch transaction where inner transactions are from different multi-sig accounts. + */ + @Test + void batchWithInnerMultiSigOuterMultiSigDifferent() throws JsonRpcClientErrorException, JsonProcessingException { + // Create two multi-sig accounts, each with their own signers + final KeyPair account1KeyPair = createRandomAccountEd25519(); + final KeyPair account1Signer1KeyPair = createRandomAccountEd25519(); + final KeyPair account1Signer2KeyPaid = createRandomAccountEd25519(); + + final KeyPair account2KeyPair = createRandomAccountEd25519(); + final KeyPair account2Signer1KeyPair = createRandomAccountEd25519(); + final KeyPair account2Signer2KeyPair = createRandomAccountEd25519(); + + final KeyPair destinationKeyPair = createRandomAccountEd25519(); + + batchWithInnerMultiSigOuterMultiSigDifferentHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, account2Signer1KeyPair, account2Signer2KeyPair, + destinationKeyPair, + BatchFlags.ofAllOrNothing() + ); + batchWithInnerMultiSigOuterMultiSigDifferentHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, account2Signer1KeyPair, account2Signer2KeyPair, + destinationKeyPair, + BatchFlags.ofOnlyOne() + ); + batchWithInnerMultiSigOuterMultiSigDifferentHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, account2Signer1KeyPair, account2Signer2KeyPair, + destinationKeyPair, + BatchFlags.ofUntilFailure() + ); + batchWithInnerMultiSigOuterMultiSigDifferentHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, account2Signer1KeyPair, account2Signer2KeyPair, + destinationKeyPair, + BatchFlags.ofIndependent() + ); + } + + /** + * Test a batch transaction where inner transactions are from different multi-sig accounts with the specified Batch + * flag. + */ + private void batchWithInnerMultiSigOuterMultiSigDifferentHelper( + final KeyPair account1KeyPair, + final KeyPair account1Signer1KeyPair, + final KeyPair account1Signer2KeyPair, + final KeyPair account2KeyPair, + final KeyPair account2Signer1KeyPair, + final KeyPair account2Signer2KeyPair, + final KeyPair destinationKeyPair, + final BatchFlags batchFlags + ) throws JsonRpcClientErrorException, JsonProcessingException { + Objects.requireNonNull(account1KeyPair); + Objects.requireNonNull(account1Signer1KeyPair); + Objects.requireNonNull(account1Signer2KeyPair); + Objects.requireNonNull(account2KeyPair); + Objects.requireNonNull(account2Signer1KeyPair); + Objects.requireNonNull(account2Signer2KeyPair); + Objects.requireNonNull(destinationKeyPair); + Objects.requireNonNull(batchFlags); + + final FeeResult feeResult = xrplClient.fee(); + + final AccountInfoResult account1Result = setupMultiSigAccount( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPair + ); + + final AccountInfoResult account2Result = setupMultiSigAccount( + account2KeyPair, account2Signer1KeyPair, account2Signer2KeyPair + ); + + // Create inner payment from account1 + final Payment innerPayment1 = createInnerPayment( + account1KeyPair.publicKey().deriveAddress(), + account1Result.accountData().sequence(), + destinationKeyPair.publicKey().deriveAddress(), + 10000 + ); + // Create inner payment from account2 + final Payment innerPayment2 = createInnerPayment( + account2KeyPair.publicKey().deriveAddress(), + account2Result.accountData().sequence(), + destinationKeyPair.publicKey().deriveAddress(), + 20000 + ); + + // Build the Batch transaction - account1 is the batch submitter + Batch unsignedBatch = Batch.builder() + .account(account1KeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeBatchFee(feeResult, UnsignedInteger.valueOf(2L))) + .sequence(account1Result.accountData().sequence()) + .flags(batchFlags) + .addRawTransactions( + RawTransactionWrapper.of(innerPayment1), + RawTransactionWrapper.of(innerPayment2) + ) + .build(); + + // /////////////// + // Inner Multisign (account2Signer1 and account2Signer2 are the inner multi-signers) + // /////////////// + + final List innerSignerWrappers = Lists.newArrayList(account2Signer1KeyPair, account2Signer2KeyPair) + .stream() + .map(keyPair -> { + final PublicKey signingPublicKey = signatureService.derivePublicKey(keyPair.privateKey()); + final Signer signer = Signer.builder() + .signingPublicKey(signingPublicKey) + .transactionSignature(signatureService.multiSignInner(keyPair.privateKey(), unsignedBatch)) + .build(); + return SignerWrapper.of(signer); + }) + .collect(Collectors.toList()); + + // /////////////// + // Outer Multisign (account1Signer1 and account1Signer2 are the outer multi-signers) + // /////////////// + + final Batch unsignedBatchWithInnerBatchSigner = Batch.builder() + .from(unsignedBatch) + .batchSigners( + Lists.newArrayList( + BatchSignerWrapper.of(BatchSigner.builder() + .account(account2KeyPair.publicKey().deriveAddress()) + .signers(innerSignerWrappers) + .build() + ))) + .build(); + + // Sign the outer batch transaction (account1 is the batch submitter) + final Signature signedOuterBatchBySigner1 = signatureService.multiSign( + account1Signer1KeyPair.privateKey(), + unsignedBatchWithInnerBatchSigner + ); + + final Signature signedOuterBatchBySigner21 = signatureService.multiSign( + account1Signer2KeyPair.privateKey(), + unsignedBatchWithInnerBatchSigner + ); + + // Sort outer signers by account address (required by XRPL) + final List outerMultiSigners = Lists.newArrayList( + Signer.builder() + // account is derived from the signing public key + .signingPublicKey(account1Signer1KeyPair.publicKey()) + .transactionSignature(signedOuterBatchBySigner1) + .build(), + Signer.builder() + // account is derived from the signing public key + .signingPublicKey(account1Signer2KeyPair.publicKey()) + .transactionSignature(signedOuterBatchBySigner21) + .build() + ); + + final MultiSignedTransaction multiSignedBatch = MultiSignedTransaction.builder() + .unsignedTransaction(unsignedBatchWithInnerBatchSigner) + .signerSet(outerMultiSigners) + .build(); + + // Submit and wait for validation + final SubmitMultiSignedResult result = xrplClient.submitMultisigned(multiSignedBatch); + assertTesSuccess(result); + final TransactionResult validatedBatch = this.scanForResult( + () -> this.getValidatedTransaction(result.transaction().hash(), Batch.class) + ); + assertTesSuccess(validatedBatch); + + // Verify metadata + verifyBatchMetadata(validatedBatch); + + // Verify the destination account received both payments + final AccountInfoResult destInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(destinationKeyPair.publicKey().deriveAddress()) + ); + assertThat(destInfo.accountData().balance()).isNotNull(); + } + + // ////////////////////// + // 5. This section tests a Batch transaction with two inner payments where each inner payment transaction is + // authorized by the same multi-sig account, and the outer transaction is from a different single-sig account. Each + // Batch flag is tested over the same test. + // ////////////////////// + + /** + * Test a batch transaction where inner transactions are from the same single-sig account, and the outer transaction + * is from a multi-sig account. + */ + @Test + void batchWithInnerMultiSigOuterSingleSig() throws JsonRpcClientErrorException, JsonProcessingException { + // Create two multi-sig accounts, each with their own signers + final KeyPair account1KeyPair = createRandomAccountEd25519(); + final KeyPair account1Signer1KeyPair = createRandomAccountEd25519(); + final KeyPair account1Signer2KeyPaid = createRandomAccountEd25519(); + + final KeyPair account2KeyPair = createRandomAccountEd25519(); + + final KeyPair destinationKeyPair = createRandomAccountEd25519(); + + batchWithInnerMultiSigOuterSingleSigHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, + destinationKeyPair, + BatchFlags.ofAllOrNothing() + ); + batchWithInnerMultiSigOuterSingleSigHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, + destinationKeyPair, + BatchFlags.ofOnlyOne() + ); + batchWithInnerMultiSigOuterSingleSigHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, + destinationKeyPair, + BatchFlags.ofUntilFailure() + ); + batchWithInnerMultiSigOuterSingleSigHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, + destinationKeyPair, + BatchFlags.ofIndependent() + ); + } + + /** + * Test a batch transaction where the inner transactions are from the same single-sig account with the specified Batch + * flag. + */ + private void batchWithInnerMultiSigOuterSingleSigHelper( + final KeyPair account1KeyPair, + final KeyPair account1Signer1KeyPair, + final KeyPair account1Signer2KeyPair, + final KeyPair account2KeyPair, + final KeyPair destinationKeyPair, + final BatchFlags batchFlags + ) throws JsonRpcClientErrorException, JsonProcessingException { + Objects.requireNonNull(account1KeyPair); + Objects.requireNonNull(account1Signer1KeyPair); + Objects.requireNonNull(account1Signer2KeyPair); + Objects.requireNonNull(account2KeyPair); + Objects.requireNonNull(destinationKeyPair); + Objects.requireNonNull(batchFlags); + + final FeeResult feeResult = xrplClient.fee(); + + final AccountInfoResult account1Result = setupMultiSigAccount( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPair + ); + + final AccountInfoResult account2Info = this.scanForResult( + () -> this.getValidatedAccountInfo(account2KeyPair.publicKey().deriveAddress()) + ); + + // Create inner payment from account1 + final Payment innerPayment1 = createInnerPayment( + account1KeyPair.publicKey().deriveAddress(), + account1Result.accountData().sequence(), + destinationKeyPair.publicKey().deriveAddress(), + 10000 + ); + // Create inner payment from account2 + final Payment innerPayment2 = createInnerPayment( + account2KeyPair.publicKey().deriveAddress(), + account2Info.accountData().sequence(), + destinationKeyPair.publicKey().deriveAddress(), + 20000 + ); + + // Build the Batch transaction - account1 is the batch submitter + Batch unsignedBatch = Batch.builder() + .account(account1KeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeBatchFee(feeResult, UnsignedInteger.valueOf(2L))) + .sequence(account1Result.accountData().sequence()) + .flags(batchFlags) + .addRawTransactions( + RawTransactionWrapper.of(innerPayment1), + RawTransactionWrapper.of(innerPayment2) + ) + .build(); + + // /////////////// + // Inner Sign (account2 is the inner single-signer) + // /////////////// + + final List signerWrappers = Lists.newArrayList( + BatchSignerWrapper.of(BatchSigner.builder() + .account(account2KeyPair.publicKey().deriveAddress()) + .signingPublicKey(account2KeyPair.publicKey()) + .transactionSignature( + signatureService.signInner(account2KeyPair.privateKey(), unsignedBatch) // <-- `signInner` is crucial here + ) + .build() + ) + ); + + // /////////////// + // Outer Multisign (account1Signer1 and account1Signer2 are the outer multi-signers) + // /////////////// + + final Batch unsignedBatchWithInnerBatchSigner = Batch.builder() + .from(unsignedBatch) + .batchSigners(signerWrappers) + .build(); + + // Sign the outer batch transaction (account1 is the batch submitter) + final Signature signedOuterBatchBySigner1 = signatureService.multiSign( + account1Signer1KeyPair.privateKey(), + unsignedBatchWithInnerBatchSigner + ); + + final Signature signedOuterBatchBySigner21 = signatureService.multiSign( + account1Signer2KeyPair.privateKey(), + unsignedBatchWithInnerBatchSigner + ); + + // Sort outer signers by account address (required by XRPL) + final List outerMultiSigners = Lists.newArrayList( + Signer.builder() + // account is derived from the signing public key + .signingPublicKey(account1Signer1KeyPair.publicKey()) + .transactionSignature(signedOuterBatchBySigner1) + .build(), + Signer.builder() + // account is derived from the signing public key + .signingPublicKey(account1Signer2KeyPair.publicKey()) + .transactionSignature(signedOuterBatchBySigner21) + .build() + ); + + final MultiSignedTransaction multiSignedBatch = MultiSignedTransaction.builder() + .unsignedTransaction(unsignedBatchWithInnerBatchSigner) + .signerSet(outerMultiSigners) + .build(); + + // Submit and wait for validation + final SubmitMultiSignedResult result = xrplClient.submitMultisigned(multiSignedBatch); + assertTesSuccess(result); + final TransactionResult validatedBatch = this.scanForResult( + () -> this.getValidatedTransaction(result.transaction().hash(), Batch.class) + ); + assertTesSuccess(validatedBatch); + + // Verify metadata + verifyBatchMetadata(validatedBatch); + + // Verify the destination account received both payments + final AccountInfoResult destInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(destinationKeyPair.publicKey().deriveAddress()) + ); + assertThat(destInfo.accountData().balance()).isNotNull(); + } + + // ////////////////////// + // 6. This section tests a Batch transaction with two inner payments where each inner payment transaction is + // authorized by the same single-sig account, and the outer transaction is authorized by a different multi-sig + // account. Each Batch flag is tested over the same test. + // ////////////////////// + + /** + * Test a batch transaction where inner transactions are from different multi-sig accounts. + */ + @Test + void batchWithInnerSingleSigOuterMultiSig() throws JsonRpcClientErrorException, JsonProcessingException { + // Create two multi-sig accounts, each with their own signers + final KeyPair account1KeyPair = createRandomAccountEd25519(); + final KeyPair account1Signer1KeyPair = createRandomAccountEd25519(); + final KeyPair account1Signer2KeyPaid = createRandomAccountEd25519(); + + final KeyPair account2KeyPair = createRandomAccountEd25519(); + final KeyPair account2Signer1KeyPair = createRandomAccountEd25519(); + final KeyPair account2Signer2KeyPair = createRandomAccountEd25519(); + + final KeyPair destinationKeyPair = createRandomAccountEd25519(); + + batchWithInnerSingleSigOuterMultiSigHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, account2Signer1KeyPair, account2Signer2KeyPair, + destinationKeyPair, + BatchFlags.ofAllOrNothing() + ); + batchWithInnerSingleSigOuterMultiSigHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, account2Signer1KeyPair, account2Signer2KeyPair, + destinationKeyPair, + BatchFlags.ofOnlyOne() + ); + batchWithInnerSingleSigOuterMultiSigHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, account2Signer1KeyPair, account2Signer2KeyPair, + destinationKeyPair, + BatchFlags.ofUntilFailure() + ); + batchWithInnerSingleSigOuterMultiSigHelper( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPaid, + account2KeyPair, account2Signer1KeyPair, account2Signer2KeyPair, + destinationKeyPair, + BatchFlags.ofIndependent() + ); + } + + /** + * Test a batch transaction where inner transactions are from different multi-sig accounts with the specified Batch + * flag. + */ + private void batchWithInnerSingleSigOuterMultiSigHelper( + final KeyPair account1KeyPair, + final KeyPair account1Signer1KeyPair, + final KeyPair account1Signer2KeyPair, + final KeyPair account2KeyPair, + final KeyPair account2Signer1KeyPair, + final KeyPair account2Signer2KeyPair, + final KeyPair destinationKeyPair, + final BatchFlags batchFlags + ) throws JsonRpcClientErrorException, JsonProcessingException { + Objects.requireNonNull(account1KeyPair); + Objects.requireNonNull(account1Signer1KeyPair); + Objects.requireNonNull(account1Signer2KeyPair); + Objects.requireNonNull(account2KeyPair); + Objects.requireNonNull(account2Signer1KeyPair); + Objects.requireNonNull(account2Signer2KeyPair); + Objects.requireNonNull(destinationKeyPair); + Objects.requireNonNull(batchFlags); + + final FeeResult feeResult = xrplClient.fee(); + + final AccountInfoResult account1Result = setupMultiSigAccount( + account1KeyPair, account1Signer1KeyPair, account1Signer2KeyPair + ); + + final AccountInfoResult account2Result = setupMultiSigAccount( + account2KeyPair, account2Signer1KeyPair, account2Signer2KeyPair + ); + + // Create inner payment from account1 + final Payment innerPayment1 = createInnerPayment( + account1KeyPair.publicKey().deriveAddress(), + account1Result.accountData().sequence(), + destinationKeyPair.publicKey().deriveAddress(), + 10000 + ); + // Create inner payment from account2 + final Payment innerPayment2 = createInnerPayment( + account2KeyPair.publicKey().deriveAddress(), + account2Result.accountData().sequence(), + destinationKeyPair.publicKey().deriveAddress(), + 20000 + ); + + // Build the Batch transaction - account1 is the batch submitter + Batch unsignedBatch = Batch.builder() + .account(account1KeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeBatchFee(feeResult, UnsignedInteger.valueOf(2L))) + .sequence(account1Result.accountData().sequence()) + .flags(batchFlags) + .addRawTransactions( + RawTransactionWrapper.of(innerPayment1), + RawTransactionWrapper.of(innerPayment2) + ) + .build(); + + // /////////////// + // Inner Multisign (account2Signer1 and account2Signer2 are the inner multi-signers) + // /////////////// + + final List innerSignerWrappers = Lists.newArrayList( + BatchSignerWrapper.of(BatchSigner.builder() + .account(account2KeyPair.publicKey().deriveAddress()) + .signingPublicKey(account2KeyPair.publicKey()) + .transactionSignature( + signatureService.signInner(account2KeyPair.privateKey(), unsignedBatch) // <-- `signInner` is crucial here + ) + .build() + ) + ); + + // /////////////// + // Outer Multisign (account1Signer1 and account1Signer2 are the outer multi-signers) + // /////////////// + + final Batch unsignedBatchWithInnerBatchSigner = Batch.builder() + .from(unsignedBatch) + .batchSigners(innerSignerWrappers) + .build(); + + // Sign the outer batch transaction (account1 is the batch submitter) + final Signature signedOuterBatchBySigner1 = signatureService.multiSign( + account1Signer1KeyPair.privateKey(), + unsignedBatchWithInnerBatchSigner + ); + + final Signature signedOuterBatchBySigner21 = signatureService.multiSign( + account1Signer2KeyPair.privateKey(), + unsignedBatchWithInnerBatchSigner + ); + + // Sort outer signers by account address (required by XRPL) + final List outerMultiSigners = Lists.newArrayList( + Signer.builder() + // account is derived from the signing public key + .signingPublicKey(account1Signer1KeyPair.publicKey()) + .transactionSignature(signedOuterBatchBySigner1) + .build(), + Signer.builder() + // account is derived from the signing public key + .signingPublicKey(account1Signer2KeyPair.publicKey()) + .transactionSignature(signedOuterBatchBySigner21) + .build() + ); + + final MultiSignedTransaction multiSignedBatch = MultiSignedTransaction.builder() + .unsignedTransaction(unsignedBatchWithInnerBatchSigner) + .signerSet(outerMultiSigners) + .build(); + + // Submit and wait for validation + final SubmitMultiSignedResult result = xrplClient.submitMultisigned(multiSignedBatch); + assertTesSuccess(result); + final TransactionResult validatedBatch = this.scanForResult( + () -> this.getValidatedTransaction(result.transaction().hash(), Batch.class) + ); + assertTesSuccess(validatedBatch); + + // Verify metadata + verifyBatchMetadata(validatedBatch); + + // Verify the destination account received both payments + final AccountInfoResult destInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(destinationKeyPair.publicKey().deriveAddress()) + ); + assertThat(destInfo.accountData().balance()).isNotNull(); + } + + // ////////////////////// + // 7. This section tests a Batch transaction with two inner payments where each inner payment transaction is + // authorized by a different single-sig account, and the outer signer is a distinct third single-sig account. Each + // Batch flag is tested over the same test. + // ////////////////////// + + /** + * Test a batch transaction where two different single-sig accounts contribute inner transactions and a third account + * signs the outer batch transaction. + */ + @Test + void batchWithTwoDifferentInnerSingleSigsPlusThirdOuterSingleSig() + throws JsonRpcClientErrorException, JsonProcessingException { + final KeyPair account1KeyPair = createRandomAccountEd25519(); + final KeyPair account2KeyPair = createRandomAccountEd25519(); + final KeyPair outerSignerKeyPair = createRandomAccountEd25519(); // Third account that signs the outer batch + final KeyPair destinationKeyPair = createRandomAccountEd25519(); + + this.batchWithTwoDifferentInnerSingleSigsPlusThirdOuterSingleSigHelper( + account1KeyPair, account2KeyPair, outerSignerKeyPair, destinationKeyPair, BatchFlags.ofAllOrNothing() + ); + this.batchWithTwoDifferentInnerSingleSigsPlusThirdOuterSingleSigHelper( + account1KeyPair, account2KeyPair, outerSignerKeyPair, destinationKeyPair, BatchFlags.ofOnlyOne() + ); + this.batchWithTwoDifferentInnerSingleSigsPlusThirdOuterSingleSigHelper( + account1KeyPair, account2KeyPair, outerSignerKeyPair, destinationKeyPair, BatchFlags.ofUntilFailure() + ); + this.batchWithTwoDifferentInnerSingleSigsPlusThirdOuterSingleSigHelper( + account1KeyPair, account2KeyPair, outerSignerKeyPair, destinationKeyPair, BatchFlags.ofIndependent() + ); + } + + /** + * Test a batch transaction with Independent mode where two different single-sig accounts contribute inner + * transactions and a third account signs the outer batch transaction. + */ + private void batchWithTwoDifferentInnerSingleSigsPlusThirdOuterSingleSigHelper( + final KeyPair innerSigner1KeyPair, + final KeyPair innerSigner2KeyPair, + final KeyPair outerSignerKeyPair, + final KeyPair destinationKeyPair, + final BatchFlags batchFlags + ) throws JsonRpcClientErrorException, JsonProcessingException { + Objects.requireNonNull(innerSigner1KeyPair); + Objects.requireNonNull(innerSigner2KeyPair); + Objects.requireNonNull(outerSignerKeyPair); + Objects.requireNonNull(destinationKeyPair); + Objects.requireNonNull(batchFlags); + + final FeeResult feeResult = xrplClient.fee(); + final AccountInfoResult account1InfoResult = this.scanForResult( + () -> this.getValidatedAccountInfo(innerSigner1KeyPair.publicKey().deriveAddress()) + ); + final AccountInfoResult account2InfoResult = this.scanForResult( + () -> this.getValidatedAccountInfo(innerSigner2KeyPair.publicKey().deriveAddress()) + ); + final AccountInfoResult outerSignerInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(outerSignerKeyPair.publicKey().deriveAddress()) + ); + + // Create inner payment from account1 + final Payment innerPayment1 = createInnerPayment( + innerSigner1KeyPair.publicKey().deriveAddress(), + account1InfoResult.accountData().sequence(), + destinationKeyPair.publicKey().deriveAddress(), + 10000 + ); + // Create inner payment from account2 + final Payment innerPayment2 = createInnerPayment( + innerSigner2KeyPair.publicKey().deriveAddress(), + account2InfoResult.accountData().sequence(), + destinationKeyPair.publicKey().deriveAddress(), + 20000 + ); + + // Build the Batch transaction with Independent mode + final Batch unsignedBatch = Batch.builder() + .account(outerSignerKeyPair.publicKey().deriveAddress()) + .signingPublicKey(outerSignerKeyPair.publicKey()) + .fee(FeeUtils.computeBatchFee(feeResult, UnsignedInteger.valueOf(2L))) + .sequence(outerSignerInfo.accountData().sequence()) + .flags(batchFlags) // <-- One crux of the test + .addRawTransactions( + RawTransactionWrapper.of(innerPayment1), + RawTransactionWrapper.of(innerPayment2) + ) + .build(); + + // /////////////// + // Inner Sign (innerSigner1 and innerSigner2 are the inner signers) + // /////////////// + + final List signerWrappers = Lists.newArrayList( + // innerSigner1 + BatchSignerWrapper.of(BatchSigner.builder() + .account(innerSigner1KeyPair.publicKey().deriveAddress()) + .signingPublicKey(innerSigner1KeyPair.publicKey()) + .transactionSignature( + signatureService.signInner(innerSigner1KeyPair.privateKey(), unsignedBatch) // <-- `signInner` is crucial here + ) + .build() + ), + // innerSigner1 + BatchSignerWrapper.of(BatchSigner.builder() + .account(innerSigner2KeyPair.publicKey().deriveAddress()) + .signingPublicKey(innerSigner2KeyPair.publicKey()) + .transactionSignature( + signatureService.signInner(innerSigner2KeyPair.privateKey(), unsignedBatch) // <-- `signInner` is crucial here + ) + .build() + ) + ); + + // /////////////// + // Inner Sign (outerSigner is the outer signer) + // /////////////// + + // Sign the outer batch transaction with the third account + final SingleSignedTransaction signedBatch = signatureService.sign( + outerSignerKeyPair.privateKey(), + Batch.builder() + .from(unsignedBatch) + .batchSigners(signerWrappers) + .build() + ); + + // Submit and wait for validation + final SubmitResult result = xrplClient.submit(signedBatch); + assertTesSuccess(result); + final TransactionResult validatedBatch = this.scanForResult( + () -> this.getValidatedTransaction(result.transactionResult().hash(), Batch.class) + ); + assertTesSuccess(validatedBatch); + + // Verify metadata + verifyBatchMetadata(validatedBatch); + + // Verify the destination account received both payments + final AccountInfoResult destInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(destinationKeyPair.publicKey().deriveAddress()) + ); + assertThat(destInfo.accountData().balance()).isNotNull(); + } + + // ////////////////////// + // Helper methods to reduce code duplication + // ////////////////////// + + /** + * Helper method to set up a multi-sig account with two signers. + * + * @param accountKeyPair The account to convert to multi-sig + * @param signer1KeyPair First signer + * @param signer2KeyPair Second signer + * + * @return AccountInfoResult after the signer list is set + */ + private AccountInfoResult setupMultiSigAccount( + KeyPair accountKeyPair, + KeyPair signer1KeyPair, + KeyPair signer2KeyPair + ) throws JsonRpcClientErrorException, JsonProcessingException { + FeeResult feeResult = xrplClient.fee(); + AccountInfoResult accountInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(accountKeyPair.publicKey().deriveAddress()) + ); + + SignerListSet signerListSet = SignerListSet.builder() + .account(accountKeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) + .sequence(accountInfo.accountData().sequence()) + .signerQuorum(UnsignedInteger.valueOf(2)) + .addSignerEntries( + SignerEntryWrapper.of( + SignerEntry.builder() + .account(signer1KeyPair.publicKey().deriveAddress()) + .signerWeight(UnsignedInteger.ONE) + .build() + ), + SignerEntryWrapper.of( + SignerEntry.builder() + .account(signer2KeyPair.publicKey().deriveAddress()) + .signerWeight(UnsignedInteger.ONE) + .build() + ) + ) + .signingPublicKey(accountKeyPair.publicKey()) + .build(); + + SingleSignedTransaction signedSignerListSet = signatureService.sign( + accountKeyPair.privateKey(), signerListSet + ); + SubmitResult signerListSetResult = xrplClient.submit(signedSignerListSet); + assertTesSuccess(signerListSetResult); + + // Wait for the signer list to be set + return scanForResult( + () -> this.getValidatedAccountInfo(accountKeyPair.publicKey().deriveAddress()), + infoResult -> infoResult.accountData().signerLists().size() == 1 + ); + } + + /** + * Helper method to create an inner payment for a batch transaction. + * + * @param sourceAddress The source account address + * @param sequence The sequence number for the payment + * @param destinationAddress The destination address + * @param amount The amount in drops + * + * @return The Payment transaction + */ + private Payment createInnerPayment( + Address sourceAddress, + UnsignedInteger sequence, + Address destinationAddress, + long amount + ) { + return Payment.builder() + .account(sourceAddress) + .fee(XrpCurrencyAmount.ofDrops(0)) + .sequence(sequence) + .destination(destinationAddress) + .amount(XrpCurrencyAmount.ofDrops(amount)) + .flags(PaymentFlags.INNER_BATCH_TXN) + .build(); + } + + /** + * Asserts that the specified batch transaction result indicates a successful TES_SUCCESS outcome. + * + * @param validatedBatch The result of a batch transaction, represented as a {@link TransactionResult}. This object + * must contain metadata with a transaction result code. Throws an exception if the metadata is + * missing or if the transaction result does not match TES_SUCCESS. + */ + private void assertTesSuccess(TransactionResult validatedBatch) { + assertThat(validatedBatch.metadata() + .orElseThrow(() -> new RuntimeException("Metadata is missing.")) + .transactionResult() + ).isEqualTo(TransactionResultCodes.TES_SUCCESS); + } + + /** + * Asserts that the specified batch transaction result indicates a successful TES_SUCCESS outcome. + * + * @param validatedBatch The result of a batch transaction, represented as a {@link SubmitResult} of type + * {@link Batch}. This object must contain metadata with a transaction result code. Throws an + * exception if the metadata is missing or if the transaction result does not match + * TES_SUCCESS. + */ + private void assertTesSuccess(SubmitResult validatedBatch) { + assertThat(validatedBatch.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + } + + /** + * Asserts that the given multi-signed batch transaction result indicates a successful TES_SUCCESS outcome. + * + * @param validatedBatch The result of a multi-signed batch transaction, represented as a + * {@link SubmitMultiSignedResult} of type {@link Batch}. This object must contain metadata with + * a transaction result code. An exception will be thrown if the metadata is missing or if the + * transaction result code does not match TES_SUCCESS. + */ + private void assertTesSuccess(SubmitMultiSignedResult validatedBatch) { + assertThat(validatedBatch.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + } + + /** + * Creates a batch transaction composed of two inner payment transactions. + * + * @param sourceKeyPair The {@link KeyPair} of the source account initiating the batch transaction. + * @param feeResult The {@link FeeResult} used to determine the appropriate transaction fee for the batch. + * @param accountSequence The sequence number of the source account used for the batch transaction. + * @param innerPayment1 The first {@link Payment} transaction to be included in the batch. + * @param innerPayment2 The second {@link Payment} transaction to be included in the batch. + * @param batchFlags The {@link BatchFlags} specifying additional settings or behaviors for the batch + * transaction. + * + * @return A {@link Batch} object representing the created batch transaction. + */ + private Batch createBatchTransaction( + final KeyPair sourceKeyPair, final FeeResult feeResult, final UnsignedInteger accountSequence, + final Payment innerPayment1, final Payment innerPayment2, final BatchFlags batchFlags + ) { + return Batch.builder() + .account(sourceKeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeBatchFee(feeResult, UnsignedInteger.valueOf(2L))) + .sequence(accountSequence) + .signingPublicKey(sourceKeyPair.publicKey()) + .flags(batchFlags) + .addRawTransactions( + RawTransactionWrapper.of(innerPayment1), + RawTransactionWrapper.of(innerPayment2) + ) + .build(); + } + + /** + * Verifies that the batch transaction metadata is properly populated and contains expected data. + * + * @param validatedBatch The validated batch transaction result + */ + private void verifyBatchMetadata(TransactionResult validatedBatch) { + // Verify metadata is present + assertThat(validatedBatch.metadata()).isPresent(); + + TransactionMetadata metadata = validatedBatch.metadata() + .orElseThrow(() -> new RuntimeException("Metadata is missing.")); + + // Verify the transaction result is success + assertThat(metadata.transactionResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + + // Verify the transaction index is set (should be >= 0) + assertThat(metadata.transactionIndex()).isNotNull(); + + // Verify affected nodes are present + // A batch transaction should affect multiple nodes: + // - The outer account's AccountRoot (for fee and sequence) + // - Each inner transaction's source AccountRoot (for sequence) + // - Each destination AccountRoot (for balance changes) + assertThat(metadata.affectedNodes()).isNotEmpty(); + + // Log metadata details for debugging + logger.info("Batch transaction metadata - TransactionIndex: {}, AffectedNodes count: {}, TransactionResult: {}", + metadata.transactionIndex(), + metadata.affectedNodes().size(), + metadata.transactionResult() + ); + } +} 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 36b7d7bea..880378dfa 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 @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; import org.xrpl.xrpl4j.crypto.keys.KeyPair; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; import org.xrpl.xrpl4j.crypto.signing.MultiSignedTransaction; import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; @@ -157,7 +158,12 @@ void addSignersToSignerListAndSendPayment() throws JsonRpcClientErrorException, ///////////////////////////// // Alice and Bob sign the transaction with their private keys using the "multiSign" method. Set signers = Lists.newArrayList(aliceKeyPair, bobKeyPair).stream() - .map(wallet -> signatureService.multiSignToSigner(wallet.privateKey(), unsignedPayment)) + // Below is the same as the now-deprecated: + // return derivedKeySignatureService.multiSignToSigner(wallet.publicKey(), unsignedPayment); + .map(wallet -> Signer.builder() + .signingPublicKey(wallet.publicKey()) + .transactionSignature(signatureService.multiSign(wallet.privateKey(), unsignedPayment)) + .build()) .collect(Collectors.toSet()); ///////////////////////////// diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitMultisignedIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitMultisignedIT.java index c7413e8a5..081f2d52b 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitMultisignedIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitMultisignedIT.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; import org.xrpl.xrpl4j.crypto.keys.KeyPair; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; import org.xrpl.xrpl4j.crypto.signing.MultiSignedTransaction; import org.xrpl.xrpl4j.crypto.signing.Signature; import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; @@ -51,7 +52,7 @@ public class SubmitMultisignedIT extends AbstractIT { - ///////////////////////////// + /// ////////////////////////// // Create four accounts, one for the multisign account owner, one for their two friends, // and one to send a Payment to. KeyPair sourceKeyPair = createRandomAccountEd25519(); @@ -66,7 +67,6 @@ public class SubmitMultisignedIT extends AbstractIT { @BeforeEach public void setUp() throws JsonRpcClientErrorException, JsonProcessingException { - ///////////////////////////// // Wait for all of the accounts to show up in a validated ledger final AccountInfoResult sourceAccountInfo = scanForResult( () -> this.getValidatedAccountInfo(sourceKeyPair.publicKey().deriveAddress()) @@ -75,11 +75,9 @@ public void setUp() throws JsonRpcClientErrorException, JsonProcessingException scanForResult(() -> this.getValidatedAccountInfo(bobKeyPair.publicKey().deriveAddress())); scanForResult(() -> this.getValidatedAccountInfo(destinationKeyPair.publicKey().deriveAddress())); - ///////////////////////////// // And validate that the source account has not set up any signer lists assertThat(sourceAccountInfo.accountData().signerLists()).isEmpty(); - ///////////////////////////// // Then submit a SignerListSet transaction to add alice and bob as signers on the account feeResult = xrplClient.fee(); SignerListSet signerListSet = SignerListSet.builder() @@ -104,7 +102,6 @@ public void setUp() throws JsonRpcClientErrorException, JsonProcessingException .signingPublicKey(sourceKeyPair.publicKey()) .build(); - ///////////////////////////// // Validate that the transaction was submitted successfully SingleSignedTransaction signedSignerListSet = signatureService.sign( sourceKeyPair.privateKey(), signerListSet @@ -117,7 +114,6 @@ public void setUp() throws JsonRpcClientErrorException, JsonProcessingException signerListSetResult.transactionResult().hash() ); - ///////////////////////////// // Then wait until the transaction enters a validated ledger and the source account's signer list // exists sourceAccountInfoAfterSignerListSet = scanForResult( @@ -138,7 +134,6 @@ public void setUp() throws JsonRpcClientErrorException, JsonProcessingException @Test public void submitMultisignedAndVerifyHash() throws JsonRpcClientErrorException, JsonProcessingException { - ///////////////////////////// // Construct an unsigned Payment transaction to be multisigned Payment unsignedPayment = Payment.builder() .account(sourceKeyPair.publicKey().deriveAddress()) @@ -153,13 +148,20 @@ public void submitMultisignedAndVerifyHash() throws JsonRpcClientErrorException, .destination(destinationKeyPair.publicKey().deriveAddress()) .build(); - ///////////////////////////// // Alice and Bob sign the transaction with their private keys List signers = Lists.newArrayList(aliceKeyPair, bobKeyPair).stream() - .map(keyPair -> signatureService.multiSignToSigner(keyPair.privateKey(), unsignedPayment)) + .map(keyPair -> { + // Below is the same as the now-deprecated: + // return signatureService.multiSignToSigner(keyPair.privateKey(), unsignedPayment); + final PublicKey signingPublicKey = signatureService.derivePublicKey(keyPair.privateKey()); + final Signature signature = signatureService.multiSign(keyPair.privateKey(), unsignedPayment); + return Signer.builder() + .signingPublicKey(signingPublicKey) + .transactionSignature(signature) + .build(); + }) .collect(Collectors.toList()); - ///////////////////////////// // Then we add the signatures to the Payment object and submit it MultiSignedTransaction signedTransaction = MultiSignedTransaction.builder() .unsignedTransaction(unsignedPayment) @@ -180,7 +182,6 @@ public void submitMultisignedAndVerifyHash() throws JsonRpcClientErrorException, @Test public void submitMultisignedWithSignersInDescOrderAndVerifyHash() throws JsonRpcClientErrorException { - ///////////////////////////// // Construct an unsigned Payment transaction to be multisigned Payment unsignedPayment = Payment.builder() .account(sourceKeyPair.publicKey().deriveAddress()) @@ -195,13 +196,20 @@ public void submitMultisignedWithSignersInDescOrderAndVerifyHash() throws JsonRp .destination(destinationKeyPair.publicKey().deriveAddress()) .build(); - ///////////////////////////// // Alice and Bob sign the transaction with their private keys List signers = Lists.newArrayList(aliceKeyPair, bobKeyPair).stream() - .map(keyPair -> signatureService.multiSignToSigner(keyPair.privateKey(), unsignedPayment)) + .map(keyPair -> { + // Below is the same as the now-deprecated: + // return signatureService.multiSignToSigner(keyPair.privateKey(), unsignedPayment); + final PublicKey signingPublicKey = signatureService.derivePublicKey(keyPair.privateKey()); + final Signature signature = signatureService.multiSign(keyPair.privateKey(), unsignedPayment); + return Signer.builder() + .signingPublicKey(signingPublicKey) + .transactionSignature(signature) + .build(); + }) .collect(Collectors.toList()); - ///////////////////////////// // Then we add the signatures to the Payment object and submit it MultiSignedTransaction signedTransaction = MultiSignedTransaction.builder() .unsignedTransaction(unsignedPayment) 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 5f63b57d6..7e6707102 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 @@ -31,6 +31,7 @@ import org.xrpl.xrpl4j.crypto.keys.PrivateKeyReference; import org.xrpl.xrpl4j.crypto.keys.PublicKey; import org.xrpl.xrpl4j.crypto.signing.MultiSignedTransaction; +import org.xrpl.xrpl4j.crypto.signing.Signature; import org.xrpl.xrpl4j.crypto.signing.SignatureService; import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; @@ -284,7 +285,16 @@ private void multiSigSendPaymentHelper( // Alice and Bob sign the transaction with their private keys using the "multiSign" method. Set signers = Lists.newArrayList(alicePrivateKeyReference, bobKeyPrivateKeyReference) .stream() - .map(privateKeyReference -> derivedKeySignatureService.multiSignToSigner(privateKeyReference, unsignedPayment)) + .map(privateKeyReference -> { + // Below is the same as the now-deprecated: + // return derivedKeySignatureService.multiSignToSigner(privateKeyReference, unsignedPayment); + final PublicKey signingPublicKey = derivedKeySignatureService.derivePublicKey(privateKeyReference); + final Signature signature = derivedKeySignatureService.multiSign(privateKeyReference, unsignedPayment); + return Signer.builder() + .signingPublicKey(signingPublicKey) + .transactionSignature(signature) + .build(); + }) .collect(Collectors.toSet()); ///////////////////////////// diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TransactUsingSignatureServiceIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TransactUsingSignatureServiceIT.java index 14af54e09..3ced29938 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TransactUsingSignatureServiceIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TransactUsingSignatureServiceIT.java @@ -31,6 +31,7 @@ import org.xrpl.xrpl4j.crypto.keys.PrivateKey; import org.xrpl.xrpl4j.crypto.keys.PublicKey; import org.xrpl.xrpl4j.crypto.signing.MultiSignedTransaction; +import org.xrpl.xrpl4j.crypto.signing.Signature; import org.xrpl.xrpl4j.crypto.signing.SignatureService; import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; @@ -254,7 +255,17 @@ private void multiSigSendPaymentHelper( ///////////////////////////// // Alice and Bob sign the transaction with their private keys using the "multiSign" method. Set signers = Lists.newArrayList(alicePrivateKey, bobPrivateKey).stream() - .map(privateKey -> signatureService.multiSignToSigner(privateKey, unsignedPayment)) + .map(privateKey -> { + // Below is the same as the now-deprecated: + // return signatureService.multiSignToSigner(privateKey, unsignedPayment); + final PublicKey signingPublicKey = signatureService.derivePublicKey(privateKey); + final Signature signature = signatureService.multiSign(privateKey, unsignedPayment); + return Signer.builder() + .signingPublicKey(signingPublicKey) + .transactionSignature(signature) + .build(); + }) + .collect(Collectors.toSet()); ///////////////////////////// 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 170061abe..3870509b8 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 @@ -30,7 +30,6 @@ import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; -import org.testcontainers.images.ImagePullPolicy; import org.testcontainers.images.PullPolicy; import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; import org.xrpl.xrpl4j.client.XrplAdminClient; @@ -49,7 +48,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; /**