> For the complete documentation index, see [llms.txt](/llms.txt)

# Transaction Handling

Transaction handling is the core purpose of a wallet. This section explains how transactions flow through the extension, from dApp request to onchain settlement.

## Transaction Flow Overview

```text
1. dApp calls wallet.sendTx(payload, options)
   ↓
2. Content script encrypts and forwards to background
   ↓
3. Background stores as pending, shows in popup
   ↓
4. User reviews and clicks "Approve"
   ↓
5. Background forwards to offscreen
   ↓
6. Offscreen:
   a. Deserializes ExecutionPayload
   b. Gets fee options (SponsoredFPC)
   c. Creates TxExecutionRequest
   d. Generates proof (WASM, 10-60 seconds)
   e. Submits to node
   ↓
7. Response flows back to dApp
```

## ExecutionPayload

The dApp sends an `ExecutionPayload` containing:

```typescript
interface ExecutionPayload {
  calls: FunctionCall[];      // Contract calls to execute
  authWitnesses: AuthWitness[]; // Pre-signed authorizations
  capsules: any[];            // Encrypted data capsules
  feePayer?: AztecAddress;    // Optional explicit fee payer
}

interface FunctionCall {
  name: string;               // Function name
  to: AztecAddress;           // Target contract
  selector: FunctionSelector; // Function to call
  type: FunctionType;         // Private, public or utility
  hideMsgSender: boolean;     // Hide the caller from an enqueued public call?
  isStatic: boolean;          // View call?
  args: Fr[];                 // Function arguments
}
```

Example from a Pod Racing dApp:

```typescript
const payload = new ExecutionPayload(
  [
    FunctionCall.from({
      name: 'boost',
      to: gameContractAddress,
      selector: await FunctionSelector.fromSignature('boost()'),
      type: FunctionType.PRIVATE,
      hideMsgSender: false,
      isStatic: false,
      args: [],
    }),
  ],
  [], // authWitnesses
  [], // capsules
);

const { receipt } = await wallet.sendTx(payload, {
  from: playerAddress,
});
```

## Approval Flow

When a wallet message arrives, the background checks whether it needs approval:

```typescript
// sendTx always requires approval — it's a state-changing operation.
// batch requires approval only if it contains a sendTx.
// Read-only calls (simulateTx, getAccounts, etc.) auto-execute.
const needsApproval =
  message.type === 'sendTx' ||
  (message.type === 'batch' &&
    Array.isArray(message.args?.[0]) &&
    message.args[0].some((m: any) => m.name === 'sendTx'));

if (needsApproval) {
  const pending = {
    sessionId: session.sessionId,
    messageId: message.messageId,
    method: message.type,
    args: message.args,
    from,
    origin: session.origin,
    timestamp: Date.now(),
  };

  pendingTransactions.push(pending);
  updateBadge();
  // User must approve in popup
}
```

The badge shows the pending count, alerting the user.

## Approval UI

The popup displays transaction details:

```typescript
function TransactionApproval({ transaction, onApprove, onReject }) {
  return (
    <div className="approval-card">
      <div className="approval-header">
        <div className="approval-origin">{new URL(transaction.origin).host}</div>
        <div className="approval-type">Send Transaction</div>
      </div>

      <div className="approval-details">
        <div className="detail-row">
          <span>From</span>
          <span>{truncateAddress(transaction.from)}</span>
        </div>

        {/* Show function calls */}
        {transaction.args?.executionPayload?.calls?.map((call, i) => (
          <div key={i} className="tx-call">
            <div>{call.name || 'Unknown'}</div>
            <div>To: {truncateAddress(call.to)}</div>
          </div>
        ))}
      </div>

      <div className="btn-group">
        <button onClick={onReject}>Reject</button>
        <button onClick={onApprove}>Approve</button>
      </div>
    </div>
  );
}
```

## Processing Approved Transactions

When the user approves:

```typescript
async function handleTransactionApproval(pending) {
  // Forward to offscreen for execution
  const result = await sendToOffscreen({
    type: MessageTypes.WALLET_METHOD,
    method: pending.method,
    args: pending.args,
    from: pending.from,
  });

  // Send response back to dApp
  await handler.sendResponse(pending.sessionId, {
    messageId: pending.messageId,
    result,
    walletId: WALLET_CONFIG.walletId,
  });

  return result;
}
```

## Offscreen Execution

The offscreen document handles the actual transaction:

```typescript
case 'sendTx': {
  const { executionPayload, options } = args;

  // 1. Deserialize the payload
  const payload = deserializeExecutionPayload(executionPayload);
  const fromAddress = AztecAddress.fromStringUnsafe(from || options.from);

  // 2. Call wallet.sendTx (inherited from BaseWallet)
  const result = await wallet.sendTx(payload, {
    ...options,
    from: fromAddress,
  });

  // 3. Serialize the result
  if ('receipt' in result) {
    return {
      txHash: result.receipt.txHash.toString(),
      status: result.receipt.status,
      blockNumber: result.receipt.blockNumber?.toString(),
    };
  }
  return { txHash: result.txHash.toString() };
}
```

note

The actual implementation uses `WalletSchema` to parse arguments in a type-safe way, as described in [PXE Integration](/developers/docs/tutorials/js_tutorials/wallet-extension/pxe-integration.md). The above is a simplified view of the logic.

## BaseWallet.sendTx

For details on how `BaseWallet.sendTx` works with `completeFeeOptions`, see [PXE Integration](/developers/docs/tutorials/js_tutorials/wallet-extension/pxe-integration.md#sponsoredfpc-fee-payment).

The inherited `sendTx` method does the heavy lifting:

```typescript
// In BaseWallet (inherited by OffscreenWallet)
async sendTx(executionPayload, opts) {
  // 1. Get fee options (our override uses SponsoredFPC!)
  const feeOptions = await this.completeFeeOptions({
    from: opts.from,
    feePayer: executionPayload.feePayer,
    gasSettings: opts.fee?.gasSettings,
  });

  // 2. Create execution request
  const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(
    executionPayload,
    opts.from,
    feeOptions
  );

  // 3. Generate proof (this is the slow part)
  const provenTx = await this.pxe.proveTx(txRequest);

  // 4. Convert to onchain transaction
  const tx = await provenTx.toTx();
  const txHash = tx.getTxHash();

  // 5. Submit to node
  await this.aztecNode.sendTx(tx);

  // 6. Optionally wait for confirmation
  if (opts.wait !== NO_WAIT) {
    const receipt = await waitForTx(this.aztecNode, txHash, opts.wait);
    return { receipt };
  }
  return { txHash };
}
```

## SponsoredFPC Integration

Our `completeFeeOptions` override in `OffscreenWallet` ensures SponsoredFPC is used, as shown in [PXE Integration](/developers/docs/tutorials/js_tutorials/wallet-extension/pxe-integration.md#sponsoredfpc-fee-payment).

The `SponsoredFeePaymentMethod`:

1. Creates a fee payment execution payload
2. Gets merged with the user's transaction
3. Calls the SponsoredFPC contract to pay fees
4. User transaction executes with paid fees

## Proof Generation

Proof generation is the slowest part (10-60 seconds):

```typescript
const provenTx = await this.pxe.proveTx(txRequest);
```

This:

1. Executes private functions locally
2. Generates WASM-based zero-knowledge proofs
3. Creates the kernel proofs
4. Packages everything for submission

The offscreen document handles this well because it:

* Doesn't have service worker timeouts
* Supports WASM
* Can use IndexedDB for intermediate state

## Simulation

For gas estimation or validation, dApps use `simulateTx`:

```typescript
case 'simulateTx': {
  const { executionPayload, options } = args;
  const payload = deserializeExecutionPayload(executionPayload);
  const fromAddress = AztecAddress.fromStringUnsafe(from || options.from);

  const result = await wallet.simulateTx(payload, {
    ...options,
    from: fromAddress,
  });

  return serializeTxSimulationResult(result);
}
```

Simulation:

* Executes the transaction locally
* Estimates gas usage
* Detects revert conditions
* Doesn't generate full proofs
* Much faster than `sendTx`

## Error Handling

Transactions can fail at several points:

```typescript
try {
  const result = await wallet.sendTx(payload, options);
  return { success: true, result };
} catch (error) {
  // Could be:
  // - Simulation failure (contract revert)
  // - Proof generation failure
  // - Node rejection (already settled, insufficient gas)
  // - Network error
  return { success: false, error: error.message };
}
```

Errors are serialized and returned to the dApp, which should handle them gracefully.

## Authorization Witnesses

For delegated actions (like approving token spending), the wallet creates auth witnesses:

```typescript
case 'createAuthWit': {
  const { from: authFrom, messageHashOrIntent } = args;
  const fromAddress = AztecAddress.fromStringUnsafe(authFrom);

  const authWit = await wallet.createAuthWit(fromAddress, messageHashOrIntent);

  return {
    requestHash: authWit.requestHash.toString(),
    witness: Array.from(authWit.witness),
  };
}
```

The account signs the authorization, which can be used by other contracts to verify permission.

## Transaction status

After submission, a transaction's receipt reports one of these statuses:

* `pending` - Still in the mempool
* `proposed` - Included in a proposed block
* `checkpointed` - The block's checkpoint has been published on L1
* `proven` - The block's checkpoint has been proven on L1
* `finalized` - The L1 transaction that proved the checkpoint has reached L1 finality
* `dropped` - Dropped by the node

See [Query transaction status](/developers/docs/aztec-js/how_to_send_transaction.md#query-transaction-status) for the receipt variants and their fields.

The wallet can wait for a transaction to be included:

```typescript
const receipt = await waitForTx(this.aztecNode, txHash, {
  timeout: 60, // Seconds
  interval: 1, // Seconds between receipt polls
});

console.log(receipt.status); // 'checkpointed' or later
console.log(receipt.blockNumber);
```

By default, `waitForTx` resolves once the transaction is `checkpointed`, and throws if the transaction is dropped or its execution reverts.

## Next Steps

With transactions flowing, let's build the [Approval UI](/developers/docs/tutorials/js_tutorials/wallet-extension/approval-ui.md) - the React popup that makes all this user-friendly.
