usePreflight
Read-only pre-signature checks — balance, native gas, and wallet chain alignment. Catches problems before the user signs anything that's doomed to fail.
usePreflight runs the checks that prevent the most common user
complaint: "I signed and it just failed". It inspects the wallet's
balance, native gas, and current chain ID against the quote on the
Review screen, and returns a list of issues — each tagged as either
blocking (Send disabled) or warning (Send proceeds, user informed).
Read-only. Zero gas. Zero transactions. Just RPC reads via wagmi and
@solana/web3.js.
Signature
function usePreflight(
quote: Quote | undefined,
walletAddress: string | undefined,
): PreflightResult;quote typically comes from state.quote when state is in review.
walletAddress comes from useWhisk().address.
Result
type PreflightCheckId = "balance" | "gas" | "chain";
type PreflightStatus = "ok" | "warning" | "blocking";
type PreflightCheck = {
id: PreflightCheckId;
status: PreflightStatus;
message: string; // short, inline-friendly
};
type PreflightResult = {
checks: PreflightCheck[]; // only failing/warning ones appear here
hasBlocking: boolean; // true → gate the Send button
isLoading: boolean; // true while balances are still being fetched
};What gets checked
| Check | Source | Status when wrong | Why |
|---|---|---|---|
| Balance | Source chain USDC/EURC/USDT balance read via wagmi | blocking | No point letting the user sign a doomed tx |
| Gas | Source chain native balance heuristic | warning | Wallets sometimes auto-fund — don't false-positive |
| Chain | wagmi's useChainId() vs source chain's evmChainId | blocking | Signing on the wrong chain confuses the wallet and the user |
Solana sources skip the chain check (no wagmi equivalent for Solana
clusters); other checks still run via useChainBalance.
Example: inline notices on a custom Review screen
"use client";
import { useWhisk, usePreflight } from "@usewhisk/react";
export function CustomReview() {
const { state, actions, address } = useWhisk();
const quote = state.kind === "review" ? state.quote : undefined;
const preflight = usePreflight(quote, address);
if (state.kind !== "review") return null;
return (
<div>
{/* …quote summary, fees, etc… */}
{preflight.checks.map((c) => (
<p
key={c.id}
className={
c.status === "blocking" ? "text-red-500" : "text-amber-500"
}
>
{c.message}
</p>
))}
<button
onClick={actions.send}
disabled={preflight.hasBlocking || preflight.isLoading}
>
{preflight.hasBlocking ? "Resolve issues above" : "Send"}
</button>
</div>
);
}The <WhiskSend> widget wires usePreflight into the default Review
screen automatically — failing checks render as inline notices, the
Send button gates on hasBlocking, and the button label flips to
"Resolve issues above" when blocked.
Notes
- The hook never throws. Network errors during balance reads surface
as
isLoading: truefor longer, then resolve tookwhen the read succeeds. - Returns an empty
checksarray (andhasBlocking: false) whenquoteis undefined or no source chain can be determined. Safe to call unconditionally. - The gas check is intentionally a heuristic, not a precise estimate. Calculating actual gas cost requires running the tx — which would defeat the point of a pre-signature check.
Related
- Error handling & recovery — the philosophy behind pre-flight as a first line of defence.
useChainBalance— the underlying balance hookusePreflightcalls.useWhisk— sources thequoteandaddressthatusePreflightconsumes.
useManualMint
Last-resort recovery hook — submit MessageTransmitter.receiveMessage directly when App Kit's retry path is exhausted but the attestation is healthy.
useTabLock
Cross-tab single-flight via BroadcastChannel — prevents accidental double-burns when the same wallet has Whisk open in more than one tab.
