whisk
Hooks

useWhiskSwap

Same-chain swap state machine, in hook form.

The state machine behind <SwapTab>. Reach for it when you want your own UI but don't want to rewrite the estimate/review/swap ceremony.

import { useWhiskSwap } from "@usewhisk/react";

What it returns

type UseWhiskSwapResult = {
  state: SwapState;
  estimate: (input: SwapInput) => Promise<void>;
  swap: () => Promise<void>;
  back: () => void;
  reset: () => void;
};

type SwapInput = {
  chain: Chain;
  tokenIn: Token | string; // alias or contract address
  tokenOut: Token | string;
  amountIn: string;
  kitKey: string;
  slippageBps?: number; // optional, falls back to App Kit's default
  stopLimit?: string; // explicit min-out, overrides slippageBps
};

type SwapState =
  | { kind: "idle" }
  | { kind: "estimating" }
  | { kind: "review"; estimate: SwapEstimate }
  | { kind: "swapping"; estimate: SwapEstimate }
  | {
      kind: "succeeded";
      estimate: SwapEstimate;
      txHash?: string;
      explorerUrl?: string;
      amountOut?: string;
    }
  | { kind: "failed"; error: WhiskError };

The hook owns the state machine. It does not own input state. You pass the full SwapInput on every estimate() call, which means you control the input UI: debouncing, slippage controls, custom token addresses, whatever fits your product. The hook just runs the state.

swap() uses the last estimate. Call it from the review state; calling it from any other kind is a no-op.

A minimal swap UI

"use client";
import { useState } from "react";
import { useWhiskSwap } from "@usewhisk/react";

export function MySwap({ kitKey }: { kitKey: string }) {
  const { state, estimate, swap, back, reset } = useWhiskSwap();
  const [amountIn, setAmountIn] = useState("");

  const onGetQuote = () =>
    estimate({
      chain: "Base",
      tokenIn: "USDC",
      tokenOut: "EURC",
      amountIn,
      kitKey,
    });

  if (state.kind === "succeeded") {
    return (
      <div>
        Done. Got {state.amountOut} {state.estimate.tokenOut}.{" "}
        <button onClick={reset}>Swap again</button>
      </div>
    );
  }

  if (state.kind === "failed") {
    return (
      <div>
        Failed: {state.error.message} <button onClick={reset}>Try again</button>
      </div>
    );
  }

  if (state.kind === "review") {
    return (
      <div>
        <p>You'll receive ≈ {state.estimate.amountOut}.</p>
        <button onClick={back}>Back</button>
        <button onClick={swap}>Confirm</button>
      </div>
    );
  }

  return (
    <div>
      <input
        value={amountIn}
        onChange={(e) => setAmountIn(e.target.value)}
        placeholder="0.00"
      />
      <button onClick={onGetQuote} disabled={state.kind !== "idle"}>
        {state.kind === "estimating" ? "Quoting…" : "Get quote"}
      </button>
      {state.kind === "swapping" ? <p>Swapping…</p> : null}
    </div>
  );
}

Slippage and stop-limit

Both live on SwapInput and are optional:

  • slippageBps is basis points the swap is willing to slip. Pass 100 for 1%, 25 for 0.25%. Omit to use App Kit's default.
  • stopLimit is an explicit minimum output, expressed in destination-token units (e.g. "0.98"). When set, it overrides slippageBps. Use this when your UI exposes a precise floor rather than a percentage.

Errors

estimate() and swap() never throw. Failures land in state.kind === "failed" with state.error as a typed WhiskError subclass:

  • ConfigError when kitKey is missing or the chain isn't swap-capable.
  • UserRejectedError when the user cancels the wallet prompt.
  • NetworkError for RPC and pricing-service failures.
  • BridgeStepError if the on-chain swap reverts.

See Errors for the full subclass set.

Need a kit key? Grab one at console.circle.com — free.

On this page