Skip to main content

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.

Available without advertisement until external audit closes. The migration primitive shipped with SDK v14.5.0 (PRs #282 + #285 + #286); mainnet smoke passed with a real on-chain receipt — see scripts/receipts/cycle-2/.

What this is

CHIPI v29 wallets (the previous default) cannot use SHHH features — no recovery, no threshold, no pluggable signers, no add-device. Migration is a one-time upgrade call that replaces the wallet’s class hash from CHIPI v29 to SHHH V8.4 without changing the wallet’s address. After migration:
  • 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).

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 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
  • The user is happy with the legacy wallet and doesn’t need recovery / threshold / pluggable signers

What gets migrated

ComponentBefore (CHIPI v29)After (SHHH V8.4)
Wallet address0x…Same 0x… (no change)
Account class hash0x053f4f87…f459f2a0x075dfb39…fa58a
Owner setOne STARK key[{ kind: "STARK", pubkey: <same STARK key>, role: "OWNER", weight: 1 }]
Session keys (SNIP-9 / SNIP-163)RegisteredPreserved — V8.4 embeds the same SNIP-163 component
Account_public_key storage slotPopulatedRead by bootstrap; cleared after
Wallet’s STARK private keyEncrypted client-sideSame key, same encryption — Chipi never sees the cleartext
On-chain balances + receiptsIntactIntact (same address)

The migration call

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>;
}
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.
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

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_DISABLEDchipi-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).
  • 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.