Pattern 1 · Stale fee cache → invalidate + retry#
ContractRevertError means the operator updated the fee posture mid-flight; your React Query cache is stale. Invalidate the relevant queries and tell the user to retry.
patterns/fee-recovery.ts
ts
import { ContractRevertError } from "@tokenops/sdk/fhe-vesting";
const queryClient = useQueryClient();
const claim = useClaim({ address: managerAddress });
claim.mutate(args, {
onError(err) {
if (err instanceof ContractRevertError) {
// Stale fee cache. Invalidate, then user retries, fresh feeInfo will
// resolve the correct branch.
queryClient.invalidateQueries({
queryKey: ["tokenops-sdk", "fhe-vesting", "managerFeeInfo"],
});
toast.info("Fee config changed. Refresh and retry.");
}
},
});Pattern 2 · Lock not lifted → render countdown#
ClaimLockedError carries err.context.unlocksAt, Unix timestamp the cliff lifts. Show a countdown; don't spam retry.
patterns/locked.ts
ts
import { ClaimLockedError } from "@tokenops/sdk/fhe-vesting";
claim.mutate(args, {
onError(err) {
if (err instanceof ClaimLockedError) {
// err.context.unlocksAt is the timestamp the lock lifts.
const secondsLeft = err.context.unlocksAt - Math.floor(Date.now() / 1000);
setCountdown(secondsLeft);
}
},
});Pattern 3 · Signature replay → request fresh sig#
AlreadyClaimedError means the admin signature was already redeemed. Single-use by design. Operator must sign a NEW Claim with a fresh nonce, surface that requirement.
patterns/sig-replay.ts
ts
import { AlreadyClaimedError } from "@tokenops/sdk/fhe-airdrop";
claim.mutate(args, {
onError(err) {
if (err instanceof AlreadyClaimedError) {
// Single-use signature already redeemed. Admin must sign a fresh
// Claim with a NEW nonce.
toast.error("This claim was already redeemed.");
promptOperator(`request_new_signature`, args.recipient);
}
},
});Pattern 4 · Preflight surface → render reasons inline#
PreflightFailedError (disperse) carries err.context.reasons[], structured blockers. Render each as its own inline alert; don't collapse to a generic toast.
patterns/preflight.ts
ts
if (err instanceof PreflightFailedError) {
setBlockers(err.context.reasons); // [{code, severity, hint, value?}]
}