Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -144,29 +144,23 @@ public String encodeForMultiSigning(String json, String xrpAccountId) throws Jso
* 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.
* <p>Per XLS-0056 V1_1, the payload is: {@code HashPrefix::Batch} + outer {@code Account} + sequence + {@code Flags}
* + count + inner tx IDs, followed by the {@code batchSignerAddress} as the per-signer suffix.</p>
*
* @return hex encoded representations
* @param batch A {@link Batch} to be encoded.
* @param batchSignerAddress The {@link Address} of the BatchSigner entry (appended as a per-signer suffix).
*
* @throws JsonProcessingException if JSON is not valid.
* @return An {@link UnsignedByteArray} with the signable bytes.
*/
public UnsignedByteArray encodeForBatchInnerSigning(Batch batch) throws JsonProcessingException {
public UnsignedByteArray encodeForBatchInnerSigning(Batch batch, Address batchSignerAddress) {
Objects.requireNonNull(batch);
Objects.requireNonNull(batchSignerAddress);
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());
UnsignedByteArray signableBytes = buildBatchSigningPayload(batch);

// Add each inner transaction ID (32 bytes each)
for (RawTransactionWrapper wrapper : batch.rawTransactions()) {
final UnsignedByteArray transactionId = computeInnerBatchTransactionId(wrapper);
signableBytes.append(transactionId);
}
// Append BatchSigner's own account ID as per-signer suffix (V1_1 single-sign suffix)
String signerAccountIdHex = new AccountIdType().fromJson(new TextNode(batchSignerAddress.value())).toHex();
signableBytes.append(UnsignedByteArray.fromHex(signerAccountIdHex));

return signableBytes;
} catch (JsonProcessingException e) {
Expand All @@ -180,32 +174,70 @@ public UnsignedByteArray encodeForBatchInnerSigning(Batch batch) throws JsonProc

/**
* 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).
* BatchSigner with nested Signers. Per XLS-0056 V1_1 / rippled's {@code checkBatchMultiSign}, the payload is the base
* batch serialization followed by {@code batchSignerAddress} then {@code nestedSignerAddress} (i.e.
* {@code finishMultiSigningData(batchSignerAddress, nestedSignerAddress)}).
*
* @return An {@link UnsignedByteArray} containing the batch serialization with account ID suffix.
* @param batch The {@link Batch} to encode.
* @param batchSignerAddress The {@link Address} of the BatchSigner entry (outer multi-sig account).
* @param nestedSignerAddress The {@link Address} of the individual signer within the BatchSigner's Signers list.
*
* @throws JsonProcessingException if there is an error processing the JSON.
* @return An {@link UnsignedByteArray} containing the batch serialization with both account ID suffixes.
*/
public UnsignedByteArray encodeForBatchInnerMultiSigning(Batch batch, Address signerAddress)
throws JsonProcessingException {
public UnsignedByteArray encodeForBatchInnerMultiSigning(
final Batch batch, final Address batchSignerAddress, final Address nestedSignerAddress
) {
Objects.requireNonNull(batch);
Objects.requireNonNull(signerAddress);
Objects.requireNonNull(batchSignerAddress);
Objects.requireNonNull(nestedSignerAddress);
try {
UnsignedByteArray result = buildBatchSigningPayload(batch);

// Start with batch serialization (HashPrefix::batch + flags + count + tx IDs)
UnsignedByteArray batchBytes = encodeForBatchInnerSigning(batch);
// Append batchSignerAddress + nestedSignerAddress (finishMultiSigningData per V1_1)
String batchSignerIdHex = new AccountIdType().fromJson(new TextNode(batchSignerAddress.value())).toHex();
result.append(UnsignedByteArray.fromHex(batchSignerIdHex));

// Create a copy to avoid mutating the original (since UnsignedByteArray.append() mutates)
UnsignedByteArray result = UnsignedByteArray.of(batchBytes.toByteArray());
String nestedSignerIdHex = new AccountIdType().fromJson(new TextNode(nestedSignerAddress.value())).toHex();
result.append(UnsignedByteArray.fromHex(nestedSignerIdHex));

// 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;
} catch (JsonProcessingException e) {
throw new RuntimeException(e.getMessage(), e);
}
}

/**
* Builds the base batch signing payload (items 1–6 from XLS-0056 V1_1 §2.1.3.2), shared by both single-sign and
* multi-sign paths: {@code HashPrefix::Batch} + outer {@code Account} + sequence + {@code Flags} + count + inner tx
* IDs.
*/
private UnsignedByteArray buildBatchSigningPayload(Batch batch) throws JsonProcessingException {
// Start with batch prefix (0x42434800 = "BCH\0")
UnsignedByteArray signableBytes = UnsignedByteArray.fromHex(XrplBinaryCodec.BATCH_SIGNATURE_PREFIX);

// Add outer account ID (20 bytes)
String accountIdHex = new AccountIdType().fromJson(new TextNode(batch.account().value())).toHex();
signableBytes.append(UnsignedByteArray.fromHex(accountIdHex));

// Add sequence value (4 bytes): TicketSequence if Sequence==0, else Sequence
final int sequenceValue = batch.sequence().longValue() == 0L ?
batch.ticketSequence().map(ts -> ts.intValue()).orElse(0) :
batch.sequence().intValue();
HashingUtils.addUInt32(signableBytes, sequenceValue);

// 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 result;
return signableBytes;
}

/**
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.Address;
import org.xrpl.xrpl4j.model.transactions.Batch;
import org.xrpl.xrpl4j.model.transactions.LoanSet;
import org.xrpl.xrpl4j.model.transactions.Signer;
Expand Down Expand Up @@ -110,8 +111,10 @@ public Signature sign(final P privateKeyable, final UnsignedClaim unsignedClaim)
}

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

@Override
Expand All @@ -120,8 +123,9 @@ public <T extends Transaction> Signature multiSign(final P privateKeyable, final
}

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

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,14 @@ public Signature sign(P privateKeyable, Attestation attestation) {
}

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

Expand All @@ -105,12 +109,16 @@ public <T extends Transaction> Signature multiSign(final P privateKeyable, final
}

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

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

return this.signatureHelper(privateKeyable, signableBytes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,21 +153,20 @@ public UnsignedByteArray toMultiSignableBytes(final Transaction transaction, fin


/**
* Convert a {@link Batch} transaction to bytes that can be signed by a Batch inner transaction signer. Per XLS-0056,
* BatchSigners sign the inner transaction in a specific format: HashPrefix::batch + flags + count + inner tx IDs.
* Convert a {@link Batch} transaction to bytes that can be signed by a Batch inner transaction signer (single-sign
* path). Per XLS-0056 V1_1, the payload is: {@code HashPrefix::Batch} + outer {@code Account} + sequence +
* {@code Flags} + count + inner tx IDs + {@code batchSignerAddress}.
*
* @param batch A {@link Batch} transaction.
* @param batch A {@link Batch} transaction.
* @param batchSignerAddress The {@link Address} of the BatchSigner entry signing this batch.
*
* @return An {@link UnsignedByteArray} containing the bytes to be signed.
*/
@Beta
public UnsignedByteArray toSignableInnerBytes(final Batch batch) {
public UnsignedByteArray toSignableInnerBytes(final Batch batch, final Address batchSignerAddress) {
Objects.requireNonNull(batch);
try {
return binaryCodec.encodeForBatchInnerSigning(batch);
} catch (JsonProcessingException e) {
throw new RuntimeException(e.getMessage(), e);
}
Objects.requireNonNull(batchSignerAddress);
return binaryCodec.encodeForBatchInnerSigning(batch, batchSignerAddress);
}

/**
Expand Down Expand Up @@ -200,22 +199,22 @@ public UnsignedByteArray toCounterpartyMultiSignableBytes(final LoanSet transact

/**
* Converts a {@link Batch} to multi-signable bytes for 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 followed by
* appending the signer's account ID.
* as a BatchSigner with nested Signers. Per XLS-0056 V1_1 / rippled's {@code checkBatchMultiSign}, the payload is the
* base batch serialization followed by {@code batchSignerAddress} then {@code nestedSignerAddress}.
*
* @param batch The {@link Batch} to convert.
* @param signerAddress The {@link Address} of the signer.
* @param batch The {@link Batch} to convert.
* @param batchSignerAddress The {@link Address} of the BatchSigner entry (outer multi-sig account).
* @param nestedSignerAddress The {@link Address} of the individual signer within the BatchSigner's Signers list.
*
* @return An {@link UnsignedByteArray} containing the batch serialization with account ID suffix.
* @return An {@link UnsignedByteArray} containing the batch serialization with both account ID suffixes.
*/
@Beta
public UnsignedByteArray toMultiSignableInnerBytes(final Batch batch, final Address signerAddress) {
public UnsignedByteArray toMultiSignableInnerBytes(
final Batch batch, final Address batchSignerAddress, final Address nestedSignerAddress
) {
Objects.requireNonNull(batch);
Objects.requireNonNull(signerAddress);
try {
return binaryCodec.encodeForBatchInnerMultiSigning(batch, signerAddress);
} catch (JsonProcessingException e) {
throw new RuntimeException(e.getMessage(), e);
}
Objects.requireNonNull(batchSignerAddress);
Objects.requireNonNull(nestedSignerAddress);
return binaryCodec.encodeForBatchInnerMultiSigning(batch, batchSignerAddress, nestedSignerAddress);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
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.Address;
import org.xrpl.xrpl4j.model.transactions.Batch;
import org.xrpl.xrpl4j.model.transactions.BatchSigner;
import org.xrpl.xrpl4j.model.transactions.LoanSet;
import org.xrpl.xrpl4j.model.transactions.Signer;
import org.xrpl.xrpl4j.model.transactions.Transaction;
Expand Down Expand Up @@ -86,19 +88,24 @@ public interface TransactionSigner<P extends PrivateKeyable> {
/**
* Get a signature for a batch transaction using the supplied {@link P}.
*
* <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.</p>
* <p>Per XLS-0056 V1_1, the payload is: {@code HashPrefix::Batch} + outer {@code Account} + sequence +
* {@code Flags} + count + inner tx IDs, followed by {@code batchSignerAddress} (the {@link BatchSigner}'s own
* account) as a per-signer suffix. Note that {@code batchSignerAddress} is <b>not</b> necessarily the address derived
* from {@code privateKeyable} — e.g. when a {@link BatchSigner} is authorized via a regular key, the signing key
* derives to the regular key's own address, but {@code batchSignerAddress} must be the underlying account's master
* address.</p>
*
* <p>This method will be marked {@link Beta} until the featureBatch amendment is enabled on mainnet.
* Its API is subject to change.</p>
*
* @param privateKeyable The {@link P} used to sign {@code batchTransaction}.
* @param batchTransaction The {@link Batch} transaction to sign.
* @param privateKeyable The {@link P} used to sign {@code batchTransaction}.
* @param batchTransaction The {@link Batch} transaction to sign.
* @param batchSignerAddress The {@link Address} of the {@link BatchSigner} entry that this signature is for.
*
* @return A {@link Signature} for the batch transaction.
*/
@Beta
Signature signInner(P privateKeyable, Batch batchTransaction);
Signature signInner(P privateKeyable, Batch batchTransaction, Address batchSignerAddress);

/**
* Get a signature for the supplied unsigned transaction using the supplied {@link P}. The primary reason this
Expand All @@ -120,25 +127,28 @@ public interface TransactionSigner<P extends PrivateKeyable> {
/**
* Obtain a multi-signature for a batch transaction using the supplied {@link P}.
*
* <p>This is used when a multi-sig account acts as a BatchSigner with nested Signers.
* Per rippled's checkBatchMultiSign, this uses batch serialization (HashPrefix::batch + flags + count + tx IDs)
* followed by appending the signer's account ID.</p>
* <p>This is used when a multi-sig account acts as a BatchSigner with nested Signers. Per XLS-0056 V1_1 /
* rippled's {@code checkBatchMultiSign}, the payload is the base batch serialization followed by
* {@code batchSignerAddress} (the outer multi-sig account) then the address derived from {@code privateKeyable} (the
* individual nested signer).</p>
*
* <p>This method will be marked {@link Beta} until the featureBatch amendment is enabled on mainnet.
* Its API is subject to change.</p>
*
* @param privateKeyable The {@link P} used to sign {@code batchTransaction}.
* @param batchTransaction The {@link Batch} transaction to sign.
* @param privateKeyable The {@link P} used to sign {@code batchTransaction}.
* @param batchTransaction The {@link Batch} transaction to sign.
* @param batchSignerAddress The {@link Address} of the BatchSigner entry (the outer multi-sig account that contains
* the individual signer in its Signers list).
*
* @return A {@link Signature} for the batch transaction with multi-sig format.
*/
Signature multiSignInner(P privateKeyable, Batch batchTransaction);
Signature multiSignInner(P privateKeyable, Batch batchTransaction, Address batchSignerAddress);

/**
* Obtain a counterparty single-signature for the supplied {@link LoanSet} transaction. The counterparty signs the
* same bytes as the first-party signer, but this method returns only the raw
* {@link Signature} rather than a {@link SingleSignedTransaction} wrapper, since the counterparty's signature is
* placed into the {@link org.xrpl.xrpl4j.model.transactions.CounterpartySignature} field, not the transaction's
* same bytes as the first-party signer, but this method returns only the raw {@link Signature} rather than a
* {@link SingleSignedTransaction} wrapper, since the counterparty's signature is placed into the
* {@link org.xrpl.xrpl4j.model.transactions.CounterpartySignature} field, not the transaction's
* {@code TxnSignature}.
*
* <p>This method will be marked {@link Beta} until the LendingProtocol amendment is enabled on mainnet. Its API
Expand All @@ -154,9 +164,9 @@ public interface TransactionSigner<P extends PrivateKeyable> {

/**
* Obtain a counterparty multi-signature for the supplied {@link LoanSet} transaction. Unlike
* {@link #multiSign(PrivateKeyable, Transaction)}, this method does <b>not</b> clear the {@code SigningPubKey}
* field, preserving the first-party signer's public key in the signed data. The resulting bytes use the same
* multi-signing prefix ({@code SMT\0}) and the counterparty signer's account ID suffix.
* {@link #multiSign(PrivateKeyable, Transaction)}, this method does <b>not</b> clear the {@code SigningPubKey} field,
* preserving the first-party signer's public key in the signed data. The resulting bytes use the same multi-signing
* prefix ({@code SMT\0}) and the counterparty signer's account ID suffix.
*
* <p>This method will be marked {@link Beta} until the LendingProtocol amendment is enabled on mainnet. Its API
* is subject to change.</p>
Expand Down
Loading
Loading