Skip to main content
A user who can’t sign anymore — broken phone, new laptop, forgotten PIN — needs a path back into their wallet. This page covers three of them:
  1. Add a new device to a wallet they can still sign with (48-hour timelock). The canonical “switching from old phone to new” case.
  2. 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.
  3. 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.
The first two flows require a SHHH V8.4 wallet; both run on-chain through the wallet itself with no off-chain custodian. The third works on any wallet type.
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:
  1. Propose — the existing owner signs an outside execution containing propose_add_owner. The on-chain wallet records a pending op tagged with OP_ADD_OWNER and starts a 48-hour timelock.
  2. Wait — the SDK exposes useGuardianRecovery().isReady(validAfter) and secondsRemaining(validAfter) to drive a countdown UI. The pending op state is read from chain — the SDK doesn’t poll on your behalf.
  3. Execute — after 48h, ANY OWNER (or anyone if the wallet is configured as such) calls execute_add_owner with the matching op_id. The wallet re-derives the proposed payload and asserts equality before mutating owner_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:
  1. Initiate — the guardian (NOT the lost owner) signs an outside execution calling initiate_recovery. The wallet starts a 7-day timelock.
  2. Wait — 7 days. During this window the original owner can cancel_recovery if they regain access — this is the safety valve against a malicious guardian.
  3. Finalize — after 7d, anyone can submit finalize_recovery. The wallet rotates owner_set to 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 pending OP_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.
This is the protection against a guardian going rogue (during recovery) or a stolen device proposing an attacker as a new owner. <Recover /> shows a Cancel button automatically when you pass an onCancel callback; pick the matching builder for the mode:

Timelock constants

These constants are re-exported on the hook return for UI display: useGuardianRecovery().TIMELOCK_SECONDS and .DEFAULT_OP_EXPIRY_SECONDS.

Who acts as guardian?

Three patterns we’ve seen integrators ship:
  1. Second wallet held by the user — a second device or a paper-backup-rooted wallet. Simplest; no third party.
  2. 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.
  3. 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.
The on-chain account is untouched — same address, same balances, same session keys. Only the encryption wrapper around the private key rotates. This flow CANNOT recover a user who forgot the old PIN (they need step 1’s ciphertext to decrypt); for that case, use the guardian recovery flow above. For migrating a PIN-only wallet to a passkey, see the useMigrateWalletToPasskey hook which wraps this same flow with the WebAuthn ceremony.