Skip to main content
Skip to main content

Browser Wallet Flows

When the user owns the private key (browser wallet via wagmi, AppKit, or any EIP-1193 provider), your app can't sign transactions on their behalf. Instead, web3agent prepares the work and your app executes each wallet action, then hands results back to continue.

This guide covers the prepare/resume loop, simulation, and the legacy intent helpers that are still available for Orbs-specific flows.


The Core Pattern

Every multi-step operation follows the same shape:

  1. Call prepareOperation() with the intent.
  2. Execute the returned wallet actions (approvals, signatures, transactions).
  3. Call resumeOperation() with the results.
  4. Repeat until result.status === "complete".
import { prepareOperation, resumeOperation } from "web3agent";

async function runSwap(userAddress: string) {
  // Step 1: Prepare
  const prepared = await prepareOperation({
    type: "swap",
    fromToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
    toToken: "0x4200000000000000000000000000000000000006",   // WETH on Base
    fromAmount: "10000000", // 10 USDC (6 decimals)
    chainId: 8453,
    userAddress,
  });

  let state = prepared;

  // Step 2+3: Execute actions and resume until done
  while (state.status !== "complete") {
    const completedActions = await executeActions(state.nextActions);

    state = await resumeOperation({
      resumeState: state.resumeState,
      completedActions,
    });
  }

  return state.result;
}

Executing Wallet Actions

prepareOperation() returns a nextActions array. Each action has a type that tells your app what the wallet needs to do.

Transaction actions

The most common action type. Submit the transaction and wait for confirmation before calling resumeOperation().

import { useWalletClient } from "wagmi";

async function executeActions(actions: WalletAction[]) {
  const walletClient = useWalletClient(); // or however you access the signer

  const completed = [];

  for (const action of actions) {
    if (action.type === "transaction") {
      const txHash = await walletClient.sendTransaction({
        to: action.to,
        data: action.data,
        value: action.value,
      });

      // Wait for the receipt — resumeOperation verifies it server-side too
      const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });

      completed.push({
        type: "transaction" as const,
        txHash,
        status: receipt.status === "success" ? "confirmed" : "failed",
      });
    }

    if (action.type === "signature") {
      const signature = await walletClient.signTypedData(action.typedData);
      completed.push({ type: "signature" as const, signature });
    }
  }

  return completed;
}

resumeOperation() re-checks the on-chain receipt before advancing. Passing status: "confirmed" is a hint, not a bypass. If the transaction reverted, the operation will surface the error.


Simulating Before Executing

Call simulateTransaction() to preview balance changes before asking the user to sign anything. This is especially useful for swaps where slippage matters.

import { simulateTransaction, prepareOperation } from "web3agent";

const prepared = await prepareOperation({
  type: "swap",
  fromToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  toToken: "0x4200000000000000000000000000000000000006",
  fromAmount: "10000000",
  chainId: 8453,
  userAddress: "0xYourAddress",
});

// Simulate the first transaction before showing the confirm dialog
const simulation = await simulateTransaction({
  chainId: 8453,
  from: "0xYourAddress",
  to: prepared.nextActions[0].to,
  data: prepared.nextActions[0].data,
  value: prepared.nextActions[0].value,
});

console.log("Expected balance changes:", simulation.balanceChanges);
// [{ token: "USDC", delta: "-10.00" }, { token: "WETH", delta: "+0.00312" }]

Show the simulation result in your UI before the user confirms. If the numbers look wrong, abort before any gas is spent.


Multi-Step Operations (Approvals + Swap)

Many operations require an ERC-20 approval before the main transaction. prepareOperation() handles this automatically by returning multiple actions in sequence.

const prepared = await prepareOperation({
  type: "swap",
  fromToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  toToken: "0x4200000000000000000000000000000000000006",
  fromAmount: "50000000", // 50 USDC
  chainId: 8453,
  userAddress: "0xYourAddress",
});

// prepared.nextActions might be:
// [
//   { type: "transaction", description: "Approve USDC", to: "0x...", data: "0x..." },
//   { type: "transaction", description: "Execute swap", to: "0x...", data: "0x..." },
// ]

// Or it might be just one action if the allowance is already sufficient.
// Your loop handles both cases identically.

The loop pattern from the first example handles this without any special casing. Execute whatever actions come back, pass the results to resumeOperation(), and repeat.


Cross-Chain Bridges

Bridge operations use prepareBridgeIntent() and follow the same staged pattern, but the "complete" state arrives after the destination chain confirms.

import { prepareBridgeIntent } from "web3agent";

const bridge = await prepareBridgeIntent({
  fromChainId: 1,       // Ethereum
  toChainId: 8453,      // Base
  fromToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
  toToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",   // USDC on Base
  fromAmount: "100000000", // 100 USDC
  userAddress: "0xYourAddress",
});

// bridge.steps describes the full route (source approval, source tx, destination wait)
// bridge.nextActions contains what the wallet needs to do right now

for (const step of bridge.steps) {
  console.log(`Step: ${step.description}`);
}

Bridge operations can take minutes. Store bridge.resumeState so the user can close the tab and check back later.


Legacy Intent Helpers

The protocol-specific helpers from earlier versions are still available. They're useful when you need direct access to Orbs-specific fields (EIP-712 typed data, approval steps, relay submission).

import {
  prepareSwapIntent,
  submitSignedSwap,
  pollSwapStatus,
  prepareTwapIntent,
  prepareLimitIntent,
  prepareOrderIntent,
  submitSignedOrder,
  getRequiredApprovals,
} from "web3agent";

// Prepare a Liquidity Hub swap intent
const intent = await prepareSwapIntent({
  fromToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  toToken: "0x4200000000000000000000000000000000000006",
  fromAmount: "10000000",
  chainId: 8453,
  userAddress: "0xYourAddress",
});

// intent.typedData — pass to wagmi's useSignTypedData
// intent.requiredApprovals — ERC-20 approvals to send first

// After the user signs:
const { orderHash } = await submitSignedSwap({
  signature: "0x...",
  body: intent.body,
});

// Poll until filled or expired
const status = await pollSwapStatus({ orderHash, chainId: 8453 });

For new projects, prefer prepareOperation() / resumeOperation(). The generic API handles more order types and chains without requiring protocol-specific knowledge.


Error Handling

Operations can fail at any stage. Always wrap the loop in a try/catch.

import { Web3AgentError } from "web3agent";

try {
  const prepared = await prepareOperation({ ... });
  // ... execute loop
} catch (e: unknown) {
  if (e instanceof Web3AgentError) {
    console.error(`Operation failed [${e.code}]: ${e.message}`);
    // e.code examples: "INSUFFICIENT_BALANCE", "SLIPPAGE_EXCEEDED", "BRIDGE_ROUTE_NOT_FOUND"
  } else {
    throw e;
  }
}

If a transaction action fails (reverted on-chain), pass status: "failed" in completedActions. resumeOperation() will surface a Web3AgentError with the revert reason.


Full Example: Swap with Simulation and Error Handling

import {
  prepareOperation,
  resumeOperation,
  simulateTransaction,
  Web3AgentError,
} from "web3agent";

async function swapWithPreview(
  fromToken: string,
  toToken: string,
  fromAmount: string,
  chainId: number,
  userAddress: string,
  walletClient: any,
  publicClient: any,
) {
  // Prepare
  const prepared = await prepareOperation({
    type: "swap",
    fromToken,
    toToken,
    fromAmount,
    chainId,
    userAddress,
  });

  // Simulate the first action
  const firstAction = prepared.nextActions[0];
  if (firstAction?.type === "transaction") {
    const sim = await simulateTransaction({
      chainId,
      from: userAddress,
      to: firstAction.to,
      data: firstAction.data,
      value: firstAction.value,
    });
    console.log("Preview:", sim.balanceChanges);
  }

  let state = prepared;

  while (state.status !== "complete") {
    const completedActions = [];

    for (const action of state.nextActions) {
      if (action.type === "transaction") {
        const txHash = await walletClient.sendTransaction({
          to: action.to,
          data: action.data,
          value: action.value,
        });
        const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
        completedActions.push({
          type: "transaction" as const,
          txHash,
          status: receipt.status === "success" ? "confirmed" : "failed",
        });
      }
    }

    state = await resumeOperation({
      resumeState: state.resumeState,
      completedActions,
    });
  }

  return state.result;
}