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

# Upgrade Existing Wallets

> Add recovery and multi-device support to existing wallets you created before v14.5.0. The wallet's address never changes — same balance, same history, same external integrations.

If you created wallets before v14.5.0, those wallets are on the legacy CHIPI v29 contract. They work fine for transfers and session keys, but they can't use recovery, can't be controlled from a second device, and can't be co-signed. Upgrade them in place — keep the user's address, balance, and history — and unlock the modern flows.

<Info>
  Available without advertisement until external audit closes. Migration shipped
  with SDK v14.5.0; mainnet smoke passed with a real on-chain receipt — see
  [scripts/receipts/cycle-2/](https://github.com/chipi-pay/sdks/tree/main/scripts/receipts/cycle-2).
</Info>

## What this is

The upgrade is one on-chain call that flips the wallet's contract class from the legacy CHIPI v29 implementation to the modern SHHH V8.4 one, **without changing the wallet's address**. After upgrade:

* All on-chain history points at the same address
* USDC balance, NFT ownership, session-keys reference the same address
* External integrators (gift recipients, x402 sellers, paymaster lanes) don't need to update anything
* Recovery, threshold, multi-device, and signer-kind flexibility all become available

Existing wallets keep working without migration — this is opt-in. New wallets created via SDK v14.5.0+ default to SHHH directly (see [overview](/services/gasless/overview)).

## When to migrate

You'd migrate when:

* A user asks to add recovery to an existing wallet (only path: migrate first, then add a guardian)
* A user wants to import a hardware passkey as the wallet's primary owner (only path: migrate first, then `propose_add_owner` with `WEBAUTHN_P256`)
* You want to defrag your tenancy onto one wallet class to simplify ops + paymaster reservations
* You're shipping the [dashboard "Migrate to SHHH" button](https://github.com/chipi-pay/sdks/issues/29) to your users

You'd NOT migrate when:

* The wallet is undeployed (counterfactual) — there's nothing to upgrade; future deploys go straight to SHHH
* The wallet is v28 or v33 (not v29) — those legacy versions need a separate path; see [chipi\_back\_migration\_inventory](https://github.com/chipi-pay/sdks/issues/28)
* The user is happy with the legacy wallet and doesn't need recovery / threshold / pluggable signers

## What gets migrated

| Component                         | Before (CHIPI v29)    | After (SHHH V8.4)                                                         |
| --------------------------------- | --------------------- | ------------------------------------------------------------------------- |
| Wallet address                    | `0x…`                 | Same `0x…` (no change)                                                    |
| Account class hash                | `0x053f4f87…f459f2a`  | `0x075dfb39…fa58a`                                                        |
| Owner set                         | One STARK key         | `[{ kind: "STARK", pubkey: <same STARK key>, role: "OWNER", weight: 1 }]` |
| Session keys (SNIP-9 / SNIP-163)  | Registered            | Preserved — V8.4 embeds the same SNIP-163 component                       |
| `Account_public_key` storage slot | Populated             | Read by bootstrap; cleared after                                          |
| Wallet's STARK private key        | Encrypted client-side | Same key, same encryption — Chipi never sees the cleartext                |
| On-chain balances + receipts      | Intact                | Intact (same address)                                                     |

## The migration call

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

function MigrateButton({ wallet }) {
  const { migrateAsync, isLoading, error } = useMigrateWalletToShhh();

  async function onClick() {
    const encryptKey = await promptForPin(); // or passkey-derived key
    const outcome = await migrateAsync({
      walletId: wallet.id,
      externalUserId: user.id,
      wallet: {
        publicKey: wallet.publicKey,
        encryptedPrivateKey: wallet.encryptedPrivateKey,
      },
      encryptKey,
      bearerToken,
    });

    if (outcome.kind === "MIGRATED") {
      showToast(`Wallet upgraded — tx ${outcome.transactionHash}`);
      if (outcome.partial) {
        showInfo("Backend will catch up shortly");
      }
    } else if (outcome.kind === "ALREADY_MIGRATED") {
      showToast(`Wallet was already on V8.4 (primary: ${outcome.primaryKind})`);
    }
  }

  return <button onClick={onClick} disabled={isLoading}>Migrate</button>;
}
```

<Warning>
  **PIN is weak — not recommended for production.**

  A user-typed PIN is a short, low-entropy string. Anyone who shoulder-surfs the PIN, observes a phishing form, or compromises the browser at typing time can decrypt the wallet's private key. PIN remains in the SDK only as a fallback recovery surface for users who lose access to their platform authenticator.

  **Production embedded-wallet apps should default to a platform passkey** (Touch ID, Face ID, Windows Hello, Android biometrics) via the `@chipi-stack/chipi-passkey` package. For SHHH V8.4 wallets, `signerKind: "WEBAUTHN_P256"` keeps the private key inside the platform authenticator — it never leaves the device, never reaches Chipi servers, and is never derived from a user-typed secret.

  Only prompt for a PIN as the encryption key when:

  * The user explicitly opted into a PIN-only flow (e.g. cold-storage / paper-backup recovery), or
  * The platform genuinely has no WebAuthn / biometric support available.

  If you are migrating an existing PIN-based wallet to a passkey, look up `useMigrateWalletToPasskey` in your framework's hook docs.
</Warning>

Under the hood:

1. The SDK calls `chipi-back`'s `POST /chipi-wallets/prepare-migrate-to-shhh`, which returns the SNIP-12 typed data for the `[upgrade(V8.4), bootstrap_from_sessions]` multicall.
2. The hook unwraps the STARK private key client-side using `encryptKey` (cleartext never crosses the wire).
3. The hook signs the typed data locally with the STARK key, then calls `POST /chipi-wallets/execute-migrate-to-shhh` with the signature.
4. `chipi-back` relays the signed multicall through the paymaster. The on-chain wallet runs `upgrade` (class hash flip) + `bootstrap_from_sessions` (owner set rebuild) atomically.
5. If chipi-back's DB patch fails after the on-chain success, the execute endpoint still returns 200 with `partial: true` so the dashboard can show "wallet upgraded; backend will catch up". The reconciler cron does the catch-up.

## Two terminal outcomes

```ts theme={null}
type MigrationOutcome =
  | {
      kind: "MIGRATED";
      walletId: string;
      walletType: "SHHH";
      signerKind: "STARK";
      classHash: string;          // SHHH V8.4 class hash
      migratedFromClassHash: string; // the legacy CHIPI v29 class hash
      transactionHash: string;
      partial?: boolean;           // true = on-chain success, DB patch deferred
    }
  | {
      kind: "ALREADY_MIGRATED";
      walletAddress: string;
      onChainClassHash: string;    // what chipi-back observed on chain
      primaryKind: string;          // the current SHHH primary signer kind
    };
```

Handle both — `ALREADY_MIGRATED` fires when the user already migrated (e.g., raced with another tab, or the wallet was migrated via the dashboard before the SDK call).

## What can go wrong

Errors the hook surfaces via `error`:

* **`STRANDED`** — the wallet's `Account_public_key` slot is empty (rare; means the wallet was created in a non-standard way). Recovery path is different — out of scope for this PR.
* **`UNDEPLOYED`** — wallet is counterfactual (never landed on chain). No migration needed; the next deploy goes straight to SHHH.
* **`DEFERRED_LEGACY`** — wallet is v28 or v33. These need their own helper before standard migration runs; tracked separately.
* **`DECRYPTION_FAILED`** — the supplied `encryptKey` doesn't decrypt the wallet's STARK key. Wrong PIN / wrong passkey.
* **`MIGRATION_DISABLED`** — `chipi-back`'s `CHIPI_MIGRATION_ENABLED` env var is off (kill switch).

## Post-migration UX

After migration:

* The wallet is `walletType: "SHHH"`, `signerKind: "STARK"`. All single-owner SHHH operations work immediately.
* To get the headline UX (passkey, recovery, threshold), the user adds those as additional owners via `propose_add_owner` (see [recovery](/services/gasless/recovery)).
* The original STARK key is still the primary owner; you can rotate it out later via `propose_remove_owner` once a passkey owner is established.

## What's NOT migrated

* **CHIPI v29 PRF-passkey wallets** keep their PRF-wrapping for the STARK key. Migrating that wallet to SHHH doesn't switch the auth model — the STARK key is still PRF-wrapped. To move the user to a true `WEBAUTHN_P256` primary, add a passkey owner via `propose_add_owner` after migration.
* **Session keys with custom `valid_until`** preserve their on-chain entries but you should re-check them against your app's policy after migration; the SDK doesn't re-validate them.

## Related

* [Overview](/services/gasless/overview) — what SHHH gives you in return
* [Passkey reference](/sdk/passkeys/api) — what owners you can add after migration
* [Recovery](/services/gasless/recovery) — the flows you unlock
