Hooks
useManualMint
Last-resort recovery hook — submit MessageTransmitter.receiveMessage directly when App Kit's retry path is exhausted but the attestation is healthy.
useManualMint drives the direct-CCTP recovery flow: poll Iris
for the attestation, then submit
MessageTransmitter.receiveMessage(message, attestation) from the
user's wallet on the destination chain.
It's the escape hatch beneath engine.retry(). Reach for it when:
- A bridge failed mid-flight (USDC burned on source, mint didn't land on destination).
engine.retry()— which wraps App Kit'skit.retryBridge— keeps failing despite the attestation being available.- You have the burn tx hash, so Iris can be polled directly.
Same gas cost as a successful retry would have been. CCTP v2's nonce
tracking on the destination MessageTransmitter prevents duplicate
mints at the protocol level, so this is safe to invoke even if a retry
might also still succeed in parallel.
Signature
function useManualMint(): {
manualMint: (input: ManualMintInput) => Promise<ManualMintResult>;
fetchAttestation: (
sourceChain: Chain,
burnTxHash: string,
) => Promise<IrisMessage>;
};Inputs
type ManualMintInput = {
/** Where to mint. Must be an EVM chain. Solana destinations return "unsupported". */
destinationChain: Chain;
/** Iris payloads. If omitted, the hook polls Iris using burnSourceChain + burnTxHash. */
message?: string;
attestation?: string;
/** Source-chain burn tx hash. Required when message / attestation are omitted. */
burnSourceChain?: Chain;
burnTxHash?: string;
/** Max time to poll Iris (ms). Default 10 minutes. */
pollTimeout?: number;
};Result
type ManualMintResult =
| { kind: "success"; txHash: string; explorerUrl?: string }
| {
kind: "failure";
reason: "no-attestation" | "unsupported-chain" | "submission-failed";
message: string;
}
| {
kind: "unsupported";
reason: "solana-destination" | "no-message-transmitter";
};success—MessageTransmitter.receiveMessagewas submitted; tx hash points at the user's destination-chain mint.failure— submission attempt failed (wallet rejection, RPC error, Iris status still pending after timeout).unsupported— chain combination Whisk can't handle today. Solana destinations fall here; the UI should fall back to the attestation-copy escape hatch.
Example: a custom failure UI
"use client";
import { useState } from "react";
import {
useWhisk,
useManualMint,
type ManualMintResult,
} from "@usewhisk/react";
export function CustomFailureSurface() {
const { state } = useWhisk();
const { manualMint } = useManualMint();
const [result, setResult] = useState<ManualMintResult | null>(null);
if (state.kind !== "failed") return null;
const burnTx = state.steps?.find(
(s) => s.name === "burn" && s.state === "success",
)?.txHash;
const route = state.quote?.route;
const sourceChain = route?.kind === "bridge" ? route.sourceChain : undefined;
const destinationChain =
route?.kind === "bridge" ? route.destinationChain : undefined;
const canManualMint =
burnTx && sourceChain && destinationChain && state.raw !== undefined;
if (!canManualMint) {
return <p>{state.error.message}</p>;
}
return (
<div>
<p>Your funds are mid-flight. Submit the mint manually:</p>
<button
onClick={async () => {
const r = await manualMint({
destinationChain,
burnSourceChain: sourceChain,
burnTxHash: burnTx,
});
setResult(r);
}}
>
Submit manually
</button>
{result?.kind === "success" && (
<p>
Submitted: <a href={result.explorerUrl}>{result.txHash}</a>
</p>
)}
{result?.kind === "failure" && <p>{result.message}</p>}
{result?.kind === "unsupported" && (
<p>
Manual recovery unavailable for this chain. Use the attestation
copy-paste escape hatch:
<code>{state.error.cause}</code>
</p>
)}
</div>
);
}Notes
- The hook calls
useWriteContractfrom wagmi, so it must be rendered inside a<WagmiProvider>(which<WhiskProvider>mounts for you whenevm()is in your config). - The destination wallet must be connected on the destination chain — wagmi will prompt the user to switch networks if they're on the source chain when they click "Submit manually". This is intentional safety: a manual mint sent to the wrong chain is unrecoverable.
- The hook does NOT call
engine.retry()internally. If you want the "try retry first, fall back to manual" flow, the<WhiskSend>widget already implements it — see Error handling & recovery.
Related
- Error handling & recovery — the broader story this hook fits into.
useWhisk—actions.retryis the higher-level retry path;useManualMintis the fallback when that also fails.- Errors reference —
WhiskError.categorytells you whether to attempt retry vs jump straight to manual mint.
