> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Recover Access

> Help users who lost their device, switched browsers, or forgot their PIN. Two on-chain flows close the lost-key dead end plus a side door for rotating a forgotten PIN.

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.

<Info>
  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/](https://github.com/chipi-pay/sdks/tree/main/scripts/receipts).
</Info>

## 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`.

```tsx theme={null}
import { Recover, useGuardianRecovery } from "@chipi-stack/chipi-react";

function AddDevice({ wallet, newOwnerPubkey }) {
  const recovery = useGuardianRecovery();
  // pendingOp comes from your chain-read transport (multicall, indexer, RPC).
  const pendingOp = useChainState(wallet.address);

  return (
    <Recover
      mode="add-device"
      walletAddress={wallet.address}
      newOwner={{ kind: "STARK", pubkeyBytes: [newOwnerPubkey], role: "OWNER", weight: 1, label: "laptop" }}
      pendingOp={pendingOp}
      onPropose={async () => {
        const call = recovery.buildProposeAddOwner({
          walletAddress: wallet.address,
          proposer: 0,
          newOwner: { kind: "STARK", pubkeyBytes: [newOwnerPubkey], role: "OWNER", weight: 1, label: "laptop" },
        });
        const txHash = await submitThroughPaymaster(call);
        return { opId, validAfter };
      }}
      onExecute={async (opId) => {
        const call = recovery.buildExecuteAddOwner({
          walletAddress: wallet.address,
          opId,
          newOwner: { kind: "STARK", pubkeyBytes: [newOwnerPubkey], role: "OWNER", weight: 1, label: "laptop" },
        });
        await submitThroughPaymaster(call);
      }}
    />
  );
}
```

## 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.

```tsx theme={null}
<Recover
  mode="guardian-recovery"
  walletAddress={lostWallet.address}
  newOwner={newOwnerFromRecoveryFlow}
  pendingOp={pendingOp}
  onPropose={async () => {
    const call = recovery.buildInitiateRecovery({
      walletAddress: lostWallet.address,
      proposer: guardianOwnerId, // owner_id of the guardian signing
      newOwner: newOwnerFromRecoveryFlow,
    });
    // Guardian signs this OE, paymaster relays.
    const { opId, validAfter } = await submitGuardianSigned(call);
    return { opId, validAfter };
  }}
  onExecute={async (opId) => {
    const call = recovery.buildFinalizeRecovery({
      walletAddress: lostWallet.address,
      newOwner: newOwnerFromRecoveryFlow,
    });
    await submitThroughPaymaster(call);
  }}
/>
```

## 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:

```tsx theme={null}
// Add-device:
<Recover
  mode="add-device"
  // …other props…
  onCancel={async (opId) => {
    const call = recovery.buildCancelPendingOp({
      walletAddress: wallet.address,
      opId,
    });
    await submitThroughPaymaster(call);
  }}
/>

// Guardian recovery:
<Recover
  mode="guardian-recovery"
  // …other props…
  onCancel={async () => {
    const call = recovery.buildCancelRecovery({
      walletAddress: lostWallet.address,
      ownerId: ownerBeingReplaced, // not an op_id
    });
    await submitThroughPaymaster(call);
  }}
/>
```

## 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 |

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](/services/gasless/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.

```ts theme={null}
import { ChipiServerSDK } from "@chipi-stack/backend";

// 1. Decrypt the existing private key with the CURRENT PIN (client-side).
//    The SDK exposes the decryption helper used by all signing flows.
// 2. Re-encrypt the same private key with the NEW key (PIN, passkey-derived,
//    or any other encryption material).
// 3. Push the new ciphertext via the wallets sub-client.
await sdk.wallets.updateWalletEncryption(
  {
    externalUserId,
    newEncryptedPrivateKey,
    // publicKey?: optional, only needed when the SDK can't infer the wallet
  },
  bearerToken,
);
```

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`](/sdk/react/hooks/use-migrate-wallet-to-passkey) hook which wraps this same flow with the WebAuthn ceremony.

## Related

* [Add a second device](/services/gasless/threshold) — proactively register a backup device before the user needs to recover
* [Upgrade existing wallets](/services/gasless/migration) — get a CHIPI v29 wallet onto SHHH first to use the on-chain recovery flows
* [When passkeys fail](/sdk/passkeys/when-passkeys-fail) — the matching browser-side playbook for passkey failure modes
