whisk
Concepts

Routing

How Whisk picks between same-chain transfer and cross-chain CCTP bridge, and what the bridge actually does.

When the user hits "Continue," Whisk has to answer one question first: is this a transfer or a bridge? The answer depends on whether the source chain equals the destination chain. For example, sending from Base to Base stays on the same chain (a direct USDC transfer). Sending from Base to Arbitrum crosses chains; the engine routes through CCTP.

Two routes

route.kindWhenWhat happens
sendSource chain = destination chain.Direct USDC ERC-20 transfer.
bridgeSource ≠ destination, both supported by CCTP.CCTP v2 burn + mint, optionally forwarded.

The decision lives in decideRoute(...) in @usewhisk/core. You almost never call it yourself, engine.quote(...) calls it and stamps the chosen route onto the returned Quote.route. But if you need to know what route a pair will take before quoting (a checkout summary, a route preview), call it directly:

import { decideRoute, isBridgeRoute } from "@usewhisk/core";

const route = decideRoute({
  sourceChain: "Arc_Testnet",
  destinationChain: "Base_Sepolia",
});

if (isBridgeRoute(route)) {
  console.log(route.steps); // ["authorize", "burn", "wait", "mint"]
}

The Forwarder Service

By default, bridges run through Circle's Iris Forwarder Service. The user signs once on the source chain. Circle relays the mint on the destination. The user never signs again, never switches chains.

The user signs an authorization on the source chain.
Whisk submits the burn transaction.

Circle's Iris service watches the source chain for that burn and submits the corresponding mint on the destination.

Whisk polls the destination chain until the mint lands, then surfaces the final tx hash.

This is on by default because the alternative is bad UX without the forwarder, the user has to switch wallets to the destination chain mid-flow and sign a second transaction.

To turn it off:

const config = createWhiskConfig({
  wallets: [evm({ projectId })],
  chains: ["Arc_Testnet", "Base_Sepolia"],
  useForwarder: false,
});

Most apps shouldn't. The only reason we expose the flag is for environments where the forwarder isn't available yet (custom Circle deployments) or for security teams that want every signature to be explicit.

What the user sees during a bridge

While state.kind is sending, the widget renders a step rail with four entries — authorize, burn, wait, mint. Each has its own status (pending | active | done | error). You can read the same thing from useWhisk if you're building a custom shell:

const { state } = useWhisk();

if (state.kind === "sending") {
  state.steps.forEach((step) => {
    console.log(step.name, step.status, step.txHash);
  });
}

txHash populates as each step lands. The final entry's hash is what you'd link to on the destination chain's explorer.

Edge cases

  • Source = destination, but bridging requested. Whisk silently downgrades to a direct transfer. The Quote.route.kind will be "send", not "bridge", even if the user picked the same chain for both pickers.
  • Bridge between two chains where CCTP doesn't exist yet. Whisk throws a ConfigError from decideRoute. Use chainsByKind or chainsByNetwork upstream to keep impossible pairs out of the UI.
  • Bridge from EVM to Solana. Supported on the chains where Circle has launched it. The forwarder behaves the same; the user still signs once on the source.

Next

How recipient lookup works.

On this page