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.kind | When | What happens |
|---|---|---|
send | Source chain = destination chain. | Direct USDC ERC-20 transfer. |
bridge | Source ≠ 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.
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.kindwill 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
ConfigErrorfromdecideRoute. UsechainsByKindorchainsByNetworkupstream 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.
