Browser Extension

Batch and Bulk Signing

Sign several transactions with one approval, or submit an atomic XLS-56 Batch.

Joey supports two different ways to handle several transactions at once. Don't confuse them:

Bulk (signTransactionBulk) XLS-56 Batch
What it is Up to 32 separate, ordinary transactions approved together One ledger transaction that carries 2–8 inner transactions
Atomic? No. Earlier transactions stay applied if a later one fails. According to the batch mode flag, for example all-or-nothing
How to send joey.signTransactionBulk({ tx_list, submit }) signTransaction or signAndSubmitTransaction with TransactionType: 'Batch'
Availability All versions When joey.supportsBatch() is true (SDK 0.5.0+)

Bulk signing

const results = await joey.signTransactionBulk({
  tx_list: [{ tx_json: trustSet }, { tx_json: payment }],
  submit: true,
})
  • One approval and one password cover the whole list. The user can't approve some entries and reject others. With a Ledger account, each transaction is still confirmed on the device.
  • submit: true signs everything first, then submits strictly in order. submit: false returns the signed blobs for you to submit.
  • Autofill reads the ledger once and assigns consecutive Sequence numbers, with staggered LastLedgerSequence values. Any Sequence you set yourself is kept.

Partial failure

If an entry fails during submission, the call rejects. The error's data describes every entry:

import type { JoeyRpcError, SignTransactionBulkFailure } from '@joeywallet/wallet-sdk'

try {
  await joey.signTransactionBulk({ tx_list, submit: true })
} catch (e) {
  const data = (e as JoeyRpcError).data as SignTransactionBulkFailure | undefined
  if (!data) throw e

  console.log('First entry that did not succeed:', data.failedIndex) // zero-based
  for (const entry of data.results) {
    switch (entry.status) {
      case 'submitted': break // validated with tesSUCCESS
      case 'failed':    break // definitely failed; see entry.engine_result
      case 'unknown':   break // may still validate: check entry.hash, don't assume failure
      case 'signed':    break // never broadcast; can still be submitted as-is, in order
      case 'stranded':  break // never broadcast and can no longer apply; sign again
    }
  }
}
  • The error message looks like transaction at index 2 of 5 did not succeed: tecUNFUNDED_PAYMENT.
  • Joey watches each submitted entry for up to 10 minutes. Entries still pending after that come back as unknown.

XLS-56 Batch

Check support first. An older extension refuses every Batch with 4100.

import { BATCH_FLAGS, TF_INNER_BATCH_TXN, type BatchTransaction } from '@joeywallet/wallet-sdk'

if (!joey.supportsBatch()) {
  // Fall back to separate transactions
}

const batch: BatchTransaction = {
  TransactionType: 'Batch',
  Account: user,
  Flags: BATCH_FLAGS.tfAllOrNothing, // exactly one mode flag
  Sequence: n,
  Fee: fee,                          // base fee × (2 + inner transactions + co-signers)
  LastLedgerSequence: current + 15,
  RawTransactions: [
    {
      RawTransaction: {
        TransactionType: 'Payment', Account: user, Destination: a, Amount: '1000000',
        Sequence: n + 1, Fee: '0', SigningPubKey: '', Flags: TF_INNER_BATCH_TXN,
      },
    },
    {
      RawTransaction: {
        TransactionType: 'Payment', Account: user, Destination: b, Amount: '2000000',
        Sequence: n + 2, Fee: '0', SigningPubKey: '', Flags: TF_INNER_BATCH_TXN,
      },
    },
  ],
}

const result = await joey.signAndSubmitTransaction({ tx_json: batch, autofill: false })

// Decide success from result.batch, never from engine_result alone
if (result.batch?.applied !== 'all') {
  // 'none' | 'some' | 'unknown'
  // Inspect result.batch.inner[i]: { hash, account, status, engine_result }
}

Warning

A rolled-back all-or-nothing batch still returns tesSUCCESS: the fee is charged but nothing is applied. Always check result.batch.applied.

Rules Joey enforces

Violations fail with -32602 before the user is asked.

  • The approval screen shows every inner transaction separately, and each must pass the same refusal rules as a normal transaction.
  • Inner transactions: Fee: "0", empty SigningPubKey, no signature, the tfInnerBatchTxn flag set, and no nested Batch. The user's inner transactions use the outer Sequence +1, +2, … and have no LastLedgerSequence.
  • Exactly one mode flag, and the Batch must be the only transaction in its request.
  • Batches that include other accounts need autofill: false, and a valid single-signature BatchSigners entry for each other account. Joey never creates a BatchSigner itself.
  • Ledger accounts can't sign a Batch.
  • With autofill on and no other accounts involved, Joey only fills fields you left out.