- Add a new device to a wallet they can still sign with (48-hour timelock). The canonical “switching from old phone to new” case.
- Recover a wallet they can no longer sign with at all (7-day timelock). A pre-registered recovery contact authorizes the rotation onto a fresh device.
- Change the PIN that encrypts their wallet’s signing key (instant). For users who remember the old PIN and just want to pick a new one — or upgrade to a passkey.
SHHH-side flows are available without advertisement until external audit closes. Both are on-chain since 2026-05-26 with shipped mainnet smokes — see scripts/receipts/.
Flow 1 — Add device (user still has wallet access)
Use when the user wants a second device controlling the same wallet (the canonical “switching from laptop to phone” case) or wants to add a guardian after onboarding. Stages:- Propose — the existing owner signs an outside execution containing
propose_add_owner. The on-chain wallet records a pending op tagged withOP_ADD_OWNERand starts a 48-hour timelock. - Wait — the SDK exposes
useGuardianRecovery().isReady(validAfter)andsecondsRemaining(validAfter)to drive a countdown UI. The pending op state is read from chain — the SDK doesn’t poll on your behalf. - Execute — after 48h, ANY OWNER (or anyone if the wallet is configured as such) calls
execute_add_ownerwith the matchingop_id. The wallet re-derives the proposed payload and asserts equality before mutatingowner_set.
Flow 2 — Guardian recovery (user lost their key)
Use when the user can no longer sign with any existing owner. A pre-registered guardian (a second user, a recovery email’s WebAuthn credential, or a paid “Guardian-as-a-Service” provider) initiates recovery on the user’s behalf. Stages:- Initiate — the guardian (NOT the lost owner) signs an outside execution calling
initiate_recovery. The wallet starts a 7-day timelock. - Wait — 7 days. During this window the original owner can
cancel_recoveryif they regain access — this is the safety valve against a malicious guardian. - Finalize — after 7d, anyone can submit
finalize_recovery. The wallet rotatesowner_setto the new owner specified in step 1.
Cancel safety valve
Within either timelock window, any current owner can cancel the pending op. The two flows take different on-chain selectors:- Add-device uses
cancel_pending_op(op_id)(matches any pendingOP_ADD_OWNER/OP_REMOVE_OWNER/OP_ROTATE_OWNER/OP_SET_THRESHOLD). - Guardian recovery uses
cancel_recovery(owner_id)— keyed by which owner is being replaced, not by an op_id, because recovery operates on the owner set directly rather than through the pending-op queue.
<Recover /> shows a Cancel button automatically when you pass an onCancel callback; pick the matching builder for the mode:
Timelock constants
| Op | Timelock | Default expiry | Why |
|---|---|---|---|
OP_ADD_OWNER | 48h | 14d | Time for the owner to notice an unexpected add-device proposal |
OP_REMOVE_OWNER | 24h | 14d | Same audit window but tighter — removal is reversible by adding back |
OP_ROTATE_OWNER | 24h | 14d | |
OP_SET_THRESHOLD | 48h | 14d | Threshold changes are governance-grade |
RECOVERY | 7d | n/a | Longest window in the system — the user must have a meaningful chance to cancel |
useGuardianRecovery().TIMELOCK_SECONDS and .DEFAULT_OP_EXPIRY_SECONDS.
Who acts as guardian?
Three patterns we’ve seen integrators ship:- Second wallet held by the user — a second device or a paper-backup-rooted wallet. Simplest; no third party.
- Email-rooted WebAuthn credential — register a passkey against the user’s email-bound device at onboarding; that credential is the guardian. The user doesn’t need a second wallet day-one.
- Guardian-as-a-Service — a paid third party (could be Chipi, could be a partner) holds a guardian key, with a published policy on when they sign recoveries (e.g., on a 48h email + 2FA confirmation). This is on our roadmap; the productization design is internal-only for now.
role: "GUARDIAN" is set at owner-add time; a guardian cannot initiate or sign transactions like an owner, only call initiate_recovery (and cancel_recovery if they want to withdraw).
What does NOT recover
Recovery rotates the wallet’s owner set. It does NOT:- Reset session keys (those remain valid until their own
valid_until) - Re-create CHIPI v29 STARK key encryption (those wallets don’t have on-chain recovery at all — see migration to move to SHHH first)
- Touch USDC balances or any other on-chain state — recovery is a key rotation, not a state rollback
Change a PIN (any wallet type)
The two on-chain flows above handle “user can’t sign at all.” A simpler flow handles “user remembers their PIN but wants to change it” — for example, they want to upgrade to a passkey, or they suspect their PIN was shoulder-surfed. This is client-side re-encryption of the same private key, then one backend call to store the new ciphertext. The wallet’s address never changes.useMigrateWalletToPasskey hook which wraps this same flow with the WebAuthn ceremony.
Related
- Add a second device — proactively register a backup device before the user needs to recover
- Upgrade existing wallets — get a CHIPI v29 wallet onto SHHH first to use the on-chain recovery flows
- When passkeys fail — the matching browser-side playbook for passkey failure modes
