@@ -144,29 +144,23 @@ public String encodeForMultiSigning(String json, String xrpAccountId) throws Jso
144144 * JSON and then checks for JsonNode values, this implementation instead accepts a well-typed Java object and operates
145145 * on that, for safety and correctness.
146146 *
147- * @param batch A {@link Batch} containing JSON to be encoded.
147+ * <p>Per XLS-0056 V1_1, the payload is: {@code HashPrefix::Batch} + outer {@code Account} + sequence + {@code Flags}
148+ * + count + inner tx IDs, followed by the {@code batchSignerAddress} as the per-signer suffix.</p>
148149 *
149- * @return hex encoded representations
150+ * @param batch A {@link Batch} to be encoded.
151+ * @param batchSignerAddress The {@link Address} of the BatchSigner entry (appended as a per-signer suffix).
150152 *
151- * @throws JsonProcessingException if JSON is not valid .
153+ * @return An {@link UnsignedByteArray} with the signable bytes .
152154 */
153- public UnsignedByteArray encodeForBatchInnerSigning (Batch batch ) throws JsonProcessingException {
155+ public UnsignedByteArray encodeForBatchInnerSigning (Batch batch , Address batchSignerAddress ) {
154156 Objects .requireNonNull (batch );
157+ Objects .requireNonNull (batchSignerAddress );
155158 try {
156- // Start with batch prefix (0x42434800 = "BCH\0")
157- UnsignedByteArray signableBytes = UnsignedByteArray .fromHex (XrplBinaryCodec .BATCH_SIGNATURE_PREFIX );
158-
159- // Add flags (4 bytes, big-endian)
160- HashingUtils .addUInt32 (signableBytes , (int ) batch .flags ().getValue ());
161-
162- // Add count of inner transactions (4 bytes, big-endian)
163- HashingUtils .addUInt32 (signableBytes , batch .rawTransactions ().size ());
159+ UnsignedByteArray signableBytes = buildBatchSigningPayload (batch );
164160
165- // Add each inner transaction ID (32 bytes each)
166- for (RawTransactionWrapper wrapper : batch .rawTransactions ()) {
167- final UnsignedByteArray transactionId = computeInnerBatchTransactionId (wrapper );
168- signableBytes .append (transactionId );
169- }
161+ // Append BatchSigner's own account ID as per-signer suffix (V1_1 single-sign suffix)
162+ String signerAccountIdHex = new AccountIdType ().fromJson (new TextNode (batchSignerAddress .value ())).toHex ();
163+ signableBytes .append (UnsignedByteArray .fromHex (signerAccountIdHex ));
170164
171165 return signableBytes ;
172166 } catch (JsonProcessingException e ) {
@@ -180,32 +174,70 @@ public UnsignedByteArray encodeForBatchInnerSigning(Batch batch) throws JsonProc
180174
181175 /**
182176 * Encode a {@link Batch} for multi-signing by a specific signer. This is used when a multi-sig account acts as a
183- * BatchSigner with nested Signers. Per rippled's checkBatchMultiSign, this uses batch serialization (serializeBatch)
184- * followed by appending the signer's account ID (finishMultiSigningData).
185- *
186- * @param batch The {@link Batch} to encode.
187- * @param signerAddress The address of the signer (will be appended as account ID suffix).
177+ * BatchSigner with nested Signers. Per XLS-0056 V1_1 / rippled's {@code checkBatchMultiSign}, the payload is the base
178+ * batch serialization followed by {@code batchSignerAddress} then {@code nestedSignerAddress} (i.e.
179+ * {@code finishMultiSigningData(batchSignerAddress, nestedSignerAddress)}).
188180 *
189- * @return An {@link UnsignedByteArray} containing the batch serialization with account ID suffix.
181+ * @param batch The {@link Batch} to encode.
182+ * @param batchSignerAddress The {@link Address} of the BatchSigner entry (outer multi-sig account).
183+ * @param nestedSignerAddress The {@link Address} of the individual signer within the BatchSigner's Signers list.
190184 *
191- * @throws JsonProcessingException if there is an error processing the JSON .
185+ * @return An {@link UnsignedByteArray} containing the batch serialization with both account ID suffixes .
192186 */
193- public UnsignedByteArray encodeForBatchInnerMultiSigning (Batch batch , Address signerAddress )
194- throws JsonProcessingException {
187+ public UnsignedByteArray encodeForBatchInnerMultiSigning (
188+ final Batch batch , final Address batchSignerAddress , final Address nestedSignerAddress
189+ ) {
195190 Objects .requireNonNull (batch );
196- Objects .requireNonNull (signerAddress );
191+ Objects .requireNonNull (batchSignerAddress );
192+ Objects .requireNonNull (nestedSignerAddress );
193+ try {
194+ UnsignedByteArray result = buildBatchSigningPayload (batch );
197195
198- // Start with batch serialization (HashPrefix::batch + flags + count + tx IDs)
199- UnsignedByteArray batchBytes = encodeForBatchInnerSigning (batch );
196+ // Append batchSignerAddress + nestedSignerAddress (finishMultiSigningData per V1_1)
197+ String batchSignerIdHex = new AccountIdType ().fromJson (new TextNode (batchSignerAddress .value ())).toHex ();
198+ result .append (UnsignedByteArray .fromHex (batchSignerIdHex ));
200199
201- // Create a copy to avoid mutating the original (since UnsignedByteArray.append() mutates)
202- UnsignedByteArray result = UnsignedByteArray . of ( batchBytes . toByteArray ( ));
200+ String nestedSignerIdHex = new AccountIdType (). fromJson ( new TextNode ( nestedSignerAddress . value ())). toHex ();
201+ result . append ( UnsignedByteArray . fromHex ( nestedSignerIdHex ));
203202
204- // Append the signer's account ID (like finishMultiSigningData does in rippled)
205- String accountIdHex = new AccountIdType ().fromJson (new TextNode (signerAddress .value ())).toHex ();
206- result .append (UnsignedByteArray .fromHex (accountIdHex ));
203+ return result ;
204+ } catch (JsonProcessingException e ) {
205+ throw new RuntimeException (e .getMessage (), e );
206+ }
207+ }
208+
209+ /**
210+ * Builds the base batch signing payload (items 1–6 from XLS-0056 V1_1 §2.1.3.2), shared by both single-sign and
211+ * multi-sign paths: {@code HashPrefix::Batch} + outer {@code Account} + sequence + {@code Flags} + count + inner tx
212+ * IDs.
213+ */
214+ private UnsignedByteArray buildBatchSigningPayload (Batch batch ) throws JsonProcessingException {
215+ // Start with batch prefix (0x42434800 = "BCH\0")
216+ UnsignedByteArray signableBytes = UnsignedByteArray .fromHex (XrplBinaryCodec .BATCH_SIGNATURE_PREFIX );
217+
218+ // Add outer account ID (20 bytes)
219+ String accountIdHex = new AccountIdType ().fromJson (new TextNode (batch .account ().value ())).toHex ();
220+ signableBytes .append (UnsignedByteArray .fromHex (accountIdHex ));
221+
222+ // Add sequence value (4 bytes): TicketSequence if Sequence==0, else Sequence
223+ final int sequenceValue = batch .sequence ().longValue () == 0L ?
224+ batch .ticketSequence ().map (ts -> ts .intValue ()).orElse (0 ) :
225+ batch .sequence ().intValue ();
226+ HashingUtils .addUInt32 (signableBytes , sequenceValue );
227+
228+ // Add flags (4 bytes, big-endian)
229+ HashingUtils .addUInt32 (signableBytes , (int ) batch .flags ().getValue ());
230+
231+ // Add count of inner transactions (4 bytes, big-endian)
232+ HashingUtils .addUInt32 (signableBytes , batch .rawTransactions ().size ());
233+
234+ // Add each inner transaction ID (32 bytes each)
235+ for (RawTransactionWrapper wrapper : batch .rawTransactions ()) {
236+ final UnsignedByteArray transactionId = computeInnerBatchTransactionId (wrapper );
237+ signableBytes .append (transactionId );
238+ }
207239
208- return result ;
240+ return signableBytes ;
209241 }
210242
211243 /**
0 commit comments