コンテンツへスキップ
最終更新:
編集

Send a Multi-Account Batch Transaction

This tutorial shows you how to create a Batch transaction containing transactions from multiple accounts, where each account must sign the Batch transaction. Any account, even one not involved in the inner transactions, can submit the batch.

Goals

By the end of this tutorial, you will be able to:

  • Create a Batch transaction with multiple inner transactions, signed by multiple accounts, and submitted by a third party account.
  • Configure the Batch transaction to ensure atomicity, so that either all inner transactions succeed or they all fail.

Prerequisites

To complete this tutorial, you should:

  • Have a basic understanding of the XRP Ledger.
  • Have an XRP Ledger client library set up in your development environment. This page provides examples for the following:

Source Code

You can find the complete source code for this tutorial's examples in the code samples section of this website's repository.

Steps

The example in this tutorial demonstrates a scenario where Bob and Charlie both owe Alice 50 XRP each, and a third-party (such as a payment processor) submits the Batch transaction atomically to ensure Alice is paid by both parties.

1. Install dependencies

From the code sample folder, use npm to install dependencies:

npm install xrpl

2. Set up client and accounts

To get started, import the client library and instantiate a client to connect to the XRPL. Then, create the accounts for Alice, Bob, Charlie, and the third-party.

import xrpl from "xrpl"

const client = new xrpl.Client("wss://s.devnet.rippletest.net:51233/")
await client.connect()

// Create and fund wallets
console.log("=== Funding new wallets from faucet... ===");
const [
  { wallet: alice },
  { wallet: bob },
  { wallet: charlie },
  { wallet: thirdPartyWallet },
] = await Promise.all([
  client.fundWallet(),
  client.fundWallet(),
  client.fundWallet(),
  client.fundWallet(),
]);

console.log(`Alice: ${alice.address}, Balance: ${await client.getXrpBalance(alice.address)} XRP`)
console.log(`Bob: ${bob.address}, Balance: ${await client.getXrpBalance(bob.address)} XRP`)
console.log(`Charlie: ${charlie.address}, Balance: ${await client.getXrpBalance(charlie.address)} XRP`)
console.log(`Third-party wallet: ${thirdPartyWallet.address}, Balance: ${await client.getXrpBalance(thirdPartyWallet.address)} XRP`)

3. Prepare inner transactions

Next, prepare the inner transactions that will be included in the batch.

// Create inner transactions  --------------------------------------------
// REQUIRED: Inner transactions MUST have the tfInnerBatchTxn flag (0x40000000).
// This marks them as part of a batch (requires Fee: 0 and empty SigningPubKey).

// Transaction 1: Charlie pays Alice
const charliePayment = {
  TransactionType: "Payment",
  Account: charlie.address,
  Destination: alice.address,
  Amount: xrpl.xrpToDrops(50),
  Flags: xrpl.GlobalFlags.tfInnerBatchTxn // THIS IS REQUIRED
}

// Transaction 2: Bob pays Alice
const bobPayment = {
  TransactionType: "Payment",
  Account: bob.address,
  Destination: alice.address,
  Amount: xrpl.xrpToDrops(50),
  Flags: xrpl.GlobalFlags.tfInnerBatchTxn // THIS IS REQUIRED
}

The first transaction sends a payment of 50 XRP from Charlie to Alice, and the second sends a payment of 50 XRP from Bob to Alice. Both transactions must include the tfInnerBatchTxn (0x40000000) flag to indicate that they are inner transactions of a batch.

Inner transactions must have a Fee of 0 and an empty string for the SigningPubKey. The outer Batch transaction handles the overall fee and signing for all inner transactions.

Note

The Fee and SigningPubKey fields are omitted as the client library's autofill functionality automatically populates these when submitting the Batch transaction.

You typically don't need to set these manually, but if you do, ensure Fee is set to 0 and SigningPubKey is an empty string.

4. Prepare Batch transaction

Create the Batch transaction and provide the inner transactions. The key fields to note are:

FieldValue
TransactionTypeThe type of transaction, in this case Batch.
AccountThe wallet address of the account that is sending the Batch transaction.
FlagsThe flags for the Batch transaction. For this example the transaction is configured with the tfAllOrNothing (0x00010000) flag to ensure that either all inner transactions succeed or they all fail atomically. See Batch Flags for other options.
RawTransactionsContains the list of inner transactions to be applied. Must include a minimum of 2 transactions and a maximum of 8 transactions. These transactions can come from one account or multiple accounts.
BatchSignersThe list of signatures required for the Batch transaction. This is required because there are multiple accounts' transactions included in the batch.
// Send Batch transaction --------------------------------------------
console.log("\n=== Creating Batch transaction... ===")
const batchTx = {
  TransactionType: "Batch",
  Account: thirdPartyWallet.address,
  Flags: xrpl.BatchFlags.tfAllOrNothing, // tfAllOrNothing: All inner transactions must succeed
  // Must include a minimum of 2 transactions and a maximum of 8 transactions.
  RawTransactions: [
    { RawTransaction: charliePayment },
    { RawTransaction: bobPayment },
  ]
}
console.log(JSON.stringify(batchTx, null, 2))

// Validate the transaction structure
xrpl.validate(batchTx)

// Set the expected number of signers, which is 2 (Bob and Charlie) in this case, for this transaction. 
// "autofill" will automatically add Fee: "0" and SigningPubKey: "" to inner transactions.
const autofilledBatchTx = await client.autofill(batchTx, 2)

Because we used autofill, the client library automatically fills in any missing fields, like Fee and SigningPubKey. Additionally, we specify the expected number of signers (2 in this case).

5. Gather batch signatures

To add the BatchSigners field, you need to collect signatures from each account that's sending a transaction within the batch. In this case we need two signatures, one from Charlie and one from Bob. Each sender must sign the Batch transaction to authorize their payment.

The xrpl.js library provides a helper function, signMultiBatch(), to sign the Batch transaction for each account.

Then, to combine the signatures into a single signed Batch transaction, use the combineBatchSigners() utility function.

// Gather batch signatures --------------------------------
// Each signer needs their own tx copy because signMultiBatch modifies the object.
// Charlie signs the Batch transaction
const charlieBatch = { ...autofilledBatchTx }
xrpl.signMultiBatch(charlie, charlieBatch)

// Bob signs the Batch transaction
const bobBatch = { ...autofilledBatchTx }
xrpl.signMultiBatch(bob, bobBatch)

// Combine inner transaction signatures.
// This returns a signed transaction blob (hex string) ready for submission.
const combinedSignedTx = xrpl.combineBatchSigners([charlieBatch, bobBatch])

6. Submit Batch transaction

With all the required signatures gathered, the third-party wallet can now submit the Batch transaction.

// Submit the signed blob with the third-party's wallet
console.log("\n=== Submitting Batch transaction... ===")
const submitResponse = await client.submitAndWait(combinedSignedTx, 
  { wallet: thirdPartyWallet }
)

7. Check Batch transaction result

To check the result of the Batch transaction submission:

// Check Batch transaction result --------------------------------
if (submitResponse.result.meta.TransactionResult !== "tesSUCCESS") {
  const resultCode = submitResponse.result.meta.TransactionResult
  console.warn(`\nTransaction failed with result code ${resultCode}`)
  await client.disconnect()
  process.exit(1)
}

console.log("\nBatch transaction submitted successfully!")
console.log("Result:\n", JSON.stringify(submitResponse.result, null, 2))
// View the transaction on the XRPL Explorer 
console.log(`\nBatch transaction URL:\nhttps://devnet.xrpl.org/transactions/${submitResponse.result.hash}`)

The code checks for a tesSUCCESS result and displays the response details.

Warning

A tesSUCCESS result indicates that the Batch transaction was processed successfully, but does not guarantee the inner transactions succeeded. For example, see the following transaction on the XRPL Explorer.

Because the Batch transaction is configured with a tfAllOrNothing flag, if any inner transaction fails, all inner transactions wil fail, and only the Batch transaction fee is deducted from the third-party wallet.

8. Verify inner transactions

Since there is no way to check the status of inner transactions in the Batch transaction result, you need to calculate the inner transaction hashes and look them up on the ledger:

// Calculate and verify inner transaction hashes --------------------------------------------
console.log("\n=== Verifying inner transactions ===")
const rawTransactions = submitResponse.result.tx_json.RawTransactions
let hasFailure = false

for (let i = 0; i < rawTransactions.length; i++) {
  const innerTx = rawTransactions[i].RawTransaction
  const hash = xrpl.hashes.hashSignedTx(innerTx)
  console.log(`\nTransaction ${i + 1} hash: ${hash}`)

  try {
    const tx = await client.request({ command: 'tx', transaction: hash })
    const status = tx.result.meta?.TransactionResult
    console.log(` - Status: ${status} (Ledger ${tx.result.ledger_index})`)
    console.log(` - Transaction URL: https://devnet.xrpl.org/transactions/${hash}`)
  } catch (error) {
    hasFailure = true
    console.log(` - Transaction not found: ${error}`)
  }
}
if (hasFailure) {
  console.error("\n--- Error: One or more inner transactions failed. ---")
  await client.disconnect()
  process.exit(1)
}

The code extracts the actual inner transactions from the batch response, calculates the hash of each inner transaction and looks up each transaction on the ledger using its hash.

9. Verify balances

You can also verify that the inner transactions executed successfully by checking the account balances to confirm the expected changes.

// Verify balances after transaction
console.log("\n=== Final balances ===")
console.log(`Alice: ${alice.address}, Balance: ${await client.getXrpBalance(alice.address)} XRP`)
console.log(`Bob: ${bob.address}, Balance: ${await client.getXrpBalance(bob.address)} XRP`)
console.log(`Charlie: ${charlie.address}, Balance: ${await client.getXrpBalance(charlie.address)} XRP`)
console.log(`Third-party wallet: ${thirdPartyWallet.address}, Balance: ${await client.getXrpBalance(thirdPartyWallet.address)} XRP`)

await client.disconnect()

See Also