Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
daafce0
Update tfInnerBatchTxn to all transactions
sappenin Jan 25, 2026
7df6bcc
Initial commit of Batch
sappenin Jan 25, 2026
81e00eb
More updates
sappenin Jan 25, 2026
df4528a
Remove IT
sappenin Jan 25, 2026
a1811c9
Add Batch IT
sappenin Jan 25, 2026
072a241
Deprecate multiSignToSigner to align with Batch and non-Batch design
sappenin Jan 28, 2026
bff5f5f
Cherry-pick Jackson fixes
sappenin Jan 28, 2026
c44365b
Fix Account exclusion logic + unit test
sappenin Jan 28, 2026
cefb207
Simplify contract for signing Batch transactions
sappenin Jan 28, 2026
151d2aa
Fix Checkstyle
sappenin Jan 28, 2026
b818155
Remove IT for other branch
sappenin Jan 28, 2026
fba6c20
Merge branch 'main' into df/batch/add-flags
sappenin Feb 3, 2026
896a2ab
Merge branch 'df/batch/add-flags' into df/batch/initial-support
sappenin Feb 3, 2026
b2a6637
Merge branch 'main' into df/batch/add-flags
sappenin Feb 4, 2026
1688d1d
Merge branch 'main' into df/batch/add-flags
sappenin Feb 4, 2026
6f75231
Merge branch 'df/batch/add-flags' of github.com:XRPLF/xrpl4j into df/…
sappenin Feb 4, 2026
a009e77
Increase coverage of BatchFlags
sappenin Feb 5, 2026
17b82d2
Improve unit test coverage
sappenin Feb 5, 2026
ca04653
Merge branch 'df/batch/add-flags' of github.com:XRPLF/xrpl4j into df/…
sappenin Feb 5, 2026
6e9a7d4
Fix checkstyle
sappenin Feb 5, 2026
02d93e5
Add additional precondition for Batch
sappenin Feb 5, 2026
a3872e9
Cleanup TODO
sappenin Feb 7, 2026
7bb33aa
Refactor txId computation to private helper
sappenin Feb 7, 2026
a087cb5
Merge branch 'main' into df/batch/add-flags
sappenin Feb 7, 2026
5d2ce0e
Merge branch 'df/batch/add-flags' into df/batch/initial-support
sappenin Feb 7, 2026
aeb43d4
Merge branch 'main' into df/batch/initial-support
sappenin Feb 10, 2026
472fdec
Fix formatting
sappenin Feb 10, 2026
9ac8f66
Enforce inner transaction check
sappenin Feb 10, 2026
35a0a62
Misc cleanup
sappenin Feb 10, 2026
72981b9
Update Batch per PR review
sappenin Feb 19, 2026
1a06b44
Add precondition checks to Batch
sappenin Feb 19, 2026
5a2bd74
Update Javadoc
sappenin Feb 19, 2026
60276e6
Add deterministic regression tests
sappenin Feb 19, 2026
0815b12
Fix checkstyle
sappenin Feb 19, 2026
b4107fb
Increase CodeCov
sappenin Feb 19, 2026
05e8e68
Fix checkstyle
sappenin Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ public <T extends XrplResult> T send(
JsonRpcRequest request,
JavaType resultType
) {
SubmitMultiSignedResult submitMultiSignedResult = SubmitMultiSignedResult.builder()
SubmitMultiSignedResult<?> submitMultiSignedResult = SubmitMultiSignedResult.builder()
.engineResult("tesSUCCESS")
.engineResultCode(200)
.engineResultMessage("Submitted")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
import org.xrpl.xrpl4j.codec.binary.types.AccountIdType;
import org.xrpl.xrpl4j.codec.binary.types.STObjectType;
import org.xrpl.xrpl4j.codec.binary.types.UInt64Type;
import org.xrpl.xrpl4j.crypto.HashingUtils;
import org.xrpl.xrpl4j.crypto.signing.SignedTransaction;
import org.xrpl.xrpl4j.model.transactions.Address;
import org.xrpl.xrpl4j.model.transactions.Batch;
import org.xrpl.xrpl4j.model.transactions.Hash256;
import org.xrpl.xrpl4j.model.transactions.RawTransactionWrapper;

import java.util.Map;
import java.util.Objects;
Expand All @@ -47,11 +53,13 @@ public class XrplBinaryCodec {
public static final String TRX_MULTI_SIGNATURE_PREFIX = "534D5400";

public static final String PAYMENT_CHANNEL_CLAIM_SIGNATURE_PREFIX = "434C4D00";
public static final String BATCH_SIGNATURE_PREFIX = "42434800"; // "BCH\0" per XLS-0056

public static final String CHANNEL_FIELD_NAME = "Channel";
public static final String AMOUNT_FIELD_NAME = "Amount";

private static final DefinitionsService definitionsService = DefinitionsService.getInstance();
private static final ObjectMapper objectMapper = BinaryCodecObjectMapperFactory.getObjectMapper();
private static final DefinitionsService DEFINITIONS_SERVICE = DefinitionsService.getInstance();
private static final ObjectMapper BINARY_CODEC_OBJECT_MAPPER = BinaryCodecObjectMapperFactory.getObjectMapper();

private static final XrplBinaryCodec INSTANCE = new XrplBinaryCodec();

Expand All @@ -70,11 +78,12 @@ public static XrplBinaryCodec getInstance() {
* @param json A {@link String} containing JSON to be encoded.
*
* @return A {@link String} containing the hex-encoded representation of {@code json}.
*
* @throws JsonProcessingException if {@code json} is not valid JSON.
*/
public String encode(String json) throws JsonProcessingException {
Objects.requireNonNull(json);
JsonNode node = BinaryCodecObjectMapperFactory.getObjectMapper().readTree(json);
JsonNode node = BINARY_CODEC_OBJECT_MAPPER.readTree(json);
return encode(node);
}

Expand All @@ -84,7 +93,6 @@ public String encode(String json) throws JsonProcessingException {
* @param jsonNode A {@link JsonNode} containing JSON to be encoded.
*
* @return A {@link String} containing the hex-encoded representation of {@code jsonNode}.
* @throws JsonProcessingException if {@code jsonNode} is not valid JSON.
*/
private String encode(final JsonNode jsonNode) {
Objects.requireNonNull(jsonNode);
Expand All @@ -99,10 +107,11 @@ private String encode(final JsonNode jsonNode) {
* @param json String containing JSON to be encoded.
*
* @return hex encoded representations
*
* @throws JsonProcessingException if JSON is not valid.
*/
public String encodeForSigning(String json) throws JsonProcessingException {
JsonNode node = BinaryCodecObjectMapperFactory.getObjectMapper().readTree(json);
JsonNode node = BINARY_CODEC_OBJECT_MAPPER.readTree(json);
return TRX_SIGNATURE_PREFIX + encode(removeNonSigningFields(node));
}

Expand All @@ -113,10 +122,11 @@ public String encodeForSigning(String json) throws JsonProcessingException {
* @param xrpAccountId A {@link String} containing the XRPL AccountId.
*
* @return hex encoded representations
*
* @throws JsonProcessingException if JSON is not valid.
*/
public String encodeForMultiSigning(String json, String xrpAccountId) throws JsonProcessingException {
JsonNode node = BinaryCodecObjectMapperFactory.getObjectMapper().readTree(json);
JsonNode node = BINARY_CODEC_OBJECT_MAPPER.readTree(json);
if (!node.isObject()) {
throw new IllegalArgumentException("JSON object required for signing");
}
Expand All @@ -126,17 +136,86 @@ public String encodeForMultiSigning(String json, String xrpAccountId) throws Jso
return TRX_MULTI_SIGNATURE_PREFIX + encode(removeNonSigningFields(node)) + suffix;
}

/**
* Encodes a {@link Batch} transaction to canonical XRPL binary as a hex string for signing purposes. Note that this
* function slightly diverges from the pattern of the other encodeForSigning functions because the bytes to be signed
* for a Batch transaction are not simply the canonical binary representation of the JSON. Instead, we have distinct
* portions of the Batch transaction that are signed. Also, unlike {@link #encodeForSigning(String)}, which accepts
* JSON and then checks for JsonNode values, this implementation instead accepts a well-typed Java object and operates
* on that, for safety and correctness.
*
* @param batch A {@link Batch} containing JSON to be encoded.
*
* @return hex encoded representations
*
* @throws JsonProcessingException if JSON is not valid.
*/
public UnsignedByteArray encodeForBatchInnerSigning(Batch batch) throws JsonProcessingException {
Objects.requireNonNull(batch);
try {
// Start with batch prefix (0x42434800 = "BCH\0")
UnsignedByteArray signableBytes = UnsignedByteArray.fromHex(XrplBinaryCodec.BATCH_SIGNATURE_PREFIX);

// Add flags (4 bytes, big-endian)
HashingUtils.addUInt32(signableBytes, (int) batch.flags().getValue());

// Add count of inner transactions (4 bytes, big-endian)
HashingUtils.addUInt32(signableBytes, batch.rawTransactions().size());

// Add each inner transaction ID (32 bytes each)
for (RawTransactionWrapper wrapper : batch.rawTransactions()) {
final UnsignedByteArray transactionId = computeInnerBatchTransactionId(wrapper);
signableBytes.append(transactionId);
}

return signableBytes;
} catch (JsonProcessingException e) {
throw new RuntimeException(e.getMessage(), e);
}
}

/**
* Encode a {@link Batch} for multi-signing by a specific signer. This is used when a multi-sig account acts as a
* BatchSigner with nested Signers. Per rippled's checkBatchMultiSign, this uses batch serialization (serializeBatch)
* followed by appending the signer's account ID (finishMultiSigningData).
*
* @param batch The {@link Batch} to encode.
* @param signerAddress The address of the signer (will be appended as account ID suffix).
*
* @return An {@link UnsignedByteArray} containing the batch serialization with account ID suffix.
*
* @throws JsonProcessingException if there is an error processing the JSON.
*/
public UnsignedByteArray encodeForBatchInnerMultiSigning(Batch batch, Address signerAddress)
throws JsonProcessingException {
Objects.requireNonNull(batch);
Objects.requireNonNull(signerAddress);

// Start with batch serialization (HashPrefix::batch + flags + count + tx IDs)
UnsignedByteArray batchBytes = encodeForBatchInnerSigning(batch);

// Create a copy to avoid mutating the original (since UnsignedByteArray.append() mutates)
UnsignedByteArray result = UnsignedByteArray.of(batchBytes.toByteArray());

// Append the signer's account ID (like finishMultiSigningData does in rippled)
String accountIdHex = new AccountIdType().fromJson(new TextNode(signerAddress.value())).toHex();
result.append(UnsignedByteArray.fromHex(accountIdHex));

return result;
}

/**
* Encodes JSON to canonical XRPL binary as a hex string for signing payment channel claims. The only JSON fields
* which will be encoded are "Channel" and "Amount".
*
* @param json String containing JSON to be encoded.
*
* @return The binary encoded JSON in hexadecimal form.
*
* @throws JsonProcessingException If the JSON is not valid.
*/
public String encodeForSigningClaim(String json) throws JsonProcessingException {
JsonNode node = BinaryCodecObjectMapperFactory.getObjectMapper().readTree(json);
JsonNode node = BINARY_CODEC_OBJECT_MAPPER.readTree(json);
if (!node.isObject()) {
throw new IllegalArgumentException("JSON object required for signing");
}
Expand Down Expand Up @@ -201,12 +280,40 @@ private JsonNode removeNonSigningFields(JsonNode node) {
.filter(this::isSigningField)
.collect(Collectors.toMap(Function.identity(), node::get));

return new ObjectNode(objectMapper.getNodeFactory(), signingFields);
return new ObjectNode(BINARY_CODEC_OBJECT_MAPPER.getNodeFactory(), signingFields);
}

private Boolean isSigningField(String fieldName) {
return definitionsService.getFieldInstance(fieldName).map(FieldInstance::isSigningField).orElse(false);
return DEFINITIONS_SERVICE.getFieldInstance(fieldName).map(FieldInstance::isSigningField).orElse(false);
}

/**
* Computes the transaction ID for an unsigned inner transaction.
*
* @param rawTransactionWrapper A {@link RawTransactionWrapper} with an unsigned inner transaction, used for Batch.
*
* @return A {@link Hash256} containing the transaction's transaction ID.
*/
private UnsignedByteArray computeInnerBatchTransactionId(final RawTransactionWrapper rawTransactionWrapper)
throws JsonProcessingException {
Objects.requireNonNull(rawTransactionWrapper);

String txJson = BINARY_CODEC_OBJECT_MAPPER.writeValueAsString(rawTransactionWrapper.rawTransaction());
// Parse the JSON and ensure SigningPubKey is set to empty string for inner transactions
JsonNode txNode = removeNonSigningFields(BINARY_CODEC_OBJECT_MAPPER.readTree(txJson));
if (txNode.isObject()) {
final ObjectNode objNode = (ObjectNode) txNode;
// Fix Flags field if it's serialized as an object instead of a number
// NOTE: Once https://github.com/XRPLF/xrpl4j/issues/649 is implemented, this line can be removed.
final JsonNode flagsNode = objNode.get("Flags");
if (flagsNode != null && flagsNode.isObject() && flagsNode.has("value")) {
objNode.put("Flags", flagsNode.get("value").asLong());
}
}
final String txHex = this.encode(txNode);
final UnsignedByteArray txBytes = UnsignedByteArray.fromHex(
SignedTransaction.SIGNED_TRANSACTION_HASH_PREFIX + txHex
);
return HashingUtils.sha512Half(txBytes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import java.util.Objects;

/**
* Hashing utilities for XRPL related hashing algorithms.
* Hashing utilities for XRPL-related hashing algorithms.
*/
public class HashingUtils {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.xrpl.xrpl4j.crypto.keys.PublicKey;
import org.xrpl.xrpl4j.model.client.channels.UnsignedClaim;
import org.xrpl.xrpl4j.model.ledger.Attestation;
import org.xrpl.xrpl4j.model.transactions.Batch;
import org.xrpl.xrpl4j.model.transactions.Signer;
import org.xrpl.xrpl4j.model.transactions.Transaction;

Expand Down Expand Up @@ -97,21 +98,31 @@ public <T extends Transaction> SingleSignedTransaction<T> sign(final P privateKe
return this.abstractTransactionSigner.sign(privateKeyable, transaction);
}

@Override
public Signature sign(final P privateKeyable, final Attestation attestation) {
return this.abstractTransactionSigner.sign(privateKeyable, attestation);
}

@Override
public Signature sign(final P privateKeyable, final UnsignedClaim unsignedClaim) {
return this.abstractTransactionSigner.sign(privateKeyable, unsignedClaim);
}

@Override
public Signature sign(P privateKeyable, Attestation attestation) {
return this.abstractTransactionSigner.sign(privateKeyable, attestation);
public Signature signInner(final P privateKeyable, final Batch batchTransaction) {
return this.abstractTransactionSigner.signInner(privateKeyable, batchTransaction);
}

@Override
public <T extends Transaction> Signature multiSign(final P privateKeyable, final T transaction) {
return abstractTransactionSigner.multiSign(privateKeyable, transaction);
}

@Override
public Signature multiSignInner(final P privateKeyable, final Batch batchTransaction) {
return abstractTransactionSigner.multiSignInner(privateKeyable, batchTransaction);
}

@Override
public <T extends Transaction> boolean verify(final Signer signer, final T unsignedTransaction) {
return abstractTransactionVerifier.verify(signer, unsignedTransaction);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.xrpl.xrpl4j.model.client.channels.UnsignedClaim;
import org.xrpl.xrpl4j.model.ledger.Attestation;
import org.xrpl.xrpl4j.model.transactions.Address;
import org.xrpl.xrpl4j.model.transactions.Batch;
import org.xrpl.xrpl4j.model.transactions.Transaction;

import java.util.Objects;
Expand Down Expand Up @@ -77,17 +78,35 @@ public Signature sign(P privateKeyable, Attestation attestation) {
return this.signingHelper(privateKeyable, signableBytes);
}

@Override
public Signature signInner(final P privateKeyable, final Batch batchTransaction) {
Objects.requireNonNull(privateKeyable);
Objects.requireNonNull(batchTransaction);
final UnsignedByteArray signableBytes = this.signatureUtils.toSignableInnerBytes(batchTransaction);
return this.signingHelper(privateKeyable, signableBytes);
}

@Override
public <T extends Transaction> Signature multiSign(final P privateKeyable, final T transaction) {
Objects.requireNonNull(privateKeyable);
Objects.requireNonNull(transaction);

final Address address = derivePublicKey(privateKeyable).deriveAddress();
final UnsignedByteArray signableTransactionBytes = this.signatureUtils.toMultiSignableBytes(transaction, address);

return this.signingHelper(privateKeyable, signableTransactionBytes);
}

@Override
public Signature multiSignInner(final P privateKeyable, final Batch batchTransaction) {
Objects.requireNonNull(privateKeyable);
Objects.requireNonNull(batchTransaction);

final Address address = derivePublicKey(privateKeyable).deriveAddress();
final UnsignedByteArray signableBytes = this.signatureUtils.toMultiSignableInnerBytes(batchTransaction, address);

return this.signingHelper(privateKeyable, signableBytes);
}

/**
* Helper to generate a signature based upon an {@link UnsignedByteArray} of transaction bytes.
*
Expand Down
Loading
Loading