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

# PIN → Passkey Migration

> Migrate existing PIN-encrypted wallets to passkey (biometric) authentication for production-grade security.

## Why migrate

PIN-encrypted wallets are the shortest path to a working integration, but they are not the right default for production. A 6-digit PIN has roughly 20 bits of entropy — an attacker who obtains the encrypted ciphertext (e.g. via a leaky metadata field, a stolen backup, or any other ciphertext exposure) can brute-force it offline in seconds.

Passkey authentication removes this vector:

* The encryption key is derived from a hardware-backed WebAuthn credential (Face ID, Touch ID, Windows Hello, security key).
* It never leaves the device's secure element.
* There is no low-entropy secret to crack.

This guide walks through migrating an existing PIN-only wallet to a passkey wallet using `useMigrateWalletToPasskey`.

<Note>
  **Recommended for all production wallets.** New wallets should be created with `usePasskey: true` from the start (see the [Passkeys guide](/sdk/guides/passkeys)). This guide covers existing wallets that were created PIN-only.
</Note>

## Prerequisites

* Wallet was created with a PIN (`encryptKey`) and currently has only `encryptedPrivateKey` set.
* User is on a device that supports WebAuthn PRF (most modern browsers + Face ID / Touch ID / Windows Hello).
* App is using `@chipi-stack/nextjs` ≥ v14.3.0 (which ships dual-key support for new wallets) or `@chipi-stack/chipi-react` ≥ v14.3.0.
* `@chipi-stack/chipi-passkey` is installed.

```bash theme={null}
npm install @chipi-stack/chipi-passkey@latest
```

## How migration works

`useMigrateWalletToPasskey` performs an atomic key swap:

1. The user's current PIN is used to decrypt `encryptedPrivateKey` locally — if the PIN is wrong, migration aborts before touching anything else.
2. A new passkey is created via WebAuthn (biometric prompt).
3. The decrypted private key is re-encrypted using the passkey-derived key.
4. The new ciphertext is sent to the backend via `PATCH /chipi-wallets/update-encryption-details`.
5. The hook returns the updated wallet and the `credentialId` — **persist both**.

The on-chain wallet address does not change. The same private key continues to control the same Starknet account; only the encryption envelope on the backend is replaced.

## Migration component

```tsx theme={null}
"use client";

import { useState } from "react";
import { useAuth } from "@clerk/nextjs";
import { useChipiWallet, useMigrateWalletToPasskey } from "@chipi-stack/nextjs";

export function MigrateToPasskey({ userId }: { userId: string }) {
  const { getToken } = useAuth();
  const { wallet } = useChipiWallet({
    externalUserId: userId,
    getBearerToken: getToken,
  });

  const { migrateWalletToPasskeyAsync, isLoading, error } = useMigrateWalletToPasskey();
  const [pin, setPin] = useState("");
  const [done, setDone] = useState(false);

  const handleMigrate = async () => {
    try {
      if (!wallet) return;
      const bearerToken = await getToken();
      if (!bearerToken) throw new Error("Missing bearer token");

      const result = await migrateWalletToPasskeyAsync({
        wallet,
        oldEncryptKey: pin,
        externalUserId: userId,
        bearerToken,
      });

      // Persist the new wallet and credentialId — these replace the old PIN-encrypted state.
      // localStorage is used here for example simplicity. In production, store both
      // values in a durable backend (your DB, or Clerk privateMetadata via a
      // server-side route) — see "After migration" below.
      localStorage.setItem("wallet", JSON.stringify(result.wallet));
      localStorage.setItem("credentialId", result.credentialId);
      setDone(true);
    } catch (e) {
      console.error("Migration failed", e);
    }
  };

  if (done) {
    return <p>✓ Migrated to passkey. You can now sign transactions with biometrics.</p>;
  }

  return (
    <div>
      <p>Upgrade your wallet to passkey-based security.</p>
      <label htmlFor="current-pin">Current PIN</label>
      <input
        id="current-pin"
        type="password"
        placeholder="Enter your current PIN"
        value={pin}
        onChange={(e) => setPin(e.target.value)}
      />
      <button onClick={handleMigrate} disabled={isLoading || !pin}>
        {isLoading ? "Migrating..." : "Migrate to Passkey"}
      </button>
      {error && <p style={{ color: "red" }}>{error.message}</p>}
    </div>
  );
}
```

## After migration

Once migration succeeds:

* `wallet.encryptedPrivateKey` is now passkey-encrypted.
* All subsequent signing flows must use `usePasskey: true`. For example:

```tsx theme={null}
import { useTransfer, ChainToken } from "@chipi-stack/nextjs";

const { transferAsync } = useTransfer();

await transferAsync({
  params: {
    wallet,
    token: ChainToken.USDC,
    recipient,
    amount,
    usePasskey: true,           // required after migration
    externalUserId: userId,
  },
  bearerToken,
});
```

* Store `credentialId` somewhere durable (your DB, Clerk `privateMetadata` via a server route, etc.). If `localStorage` is cleared, the SDK can recover the credential from the backend via `/chipi-wallets/credential-recovery`.

## A note on PIN backup

`useMigrateWalletToPasskey` performs a **single-key swap** — after migration, the backend stores only the passkey-encrypted ciphertext. A wallet that was created PIN-only does not gain a PIN backup column through migration.

If you want true dual-key (passkey primary, PIN backup), the path is to create new wallets with `usePasskey: true` from the start (see [Passkeys → Create Wallet with Passkey + PIN](/sdk/guides/passkeys)). This sets both `encryptedPrivateKey` (passkey) and `encryptedPrivateKeyBackup` (PIN) at creation time, and the SDK's `useTransfer` will automatically fall back to PIN if the passkey is unavailable.

For migrated wallets, plan for passkey recovery via WebAuthn discoverable credentials and `credentialId` recovery rather than a PIN fallback.

## Rollout strategy

A staged rollout reduces risk:

1. **Detect.** On wallet load, check whether the wallet has a `credentialId` / passkey-encrypted state. If not, surface a one-time migration prompt.
2. **Prompt at a low-stakes moment.** Don't block the user mid-transfer. Show the upgrade flow on the next wallet screen visit, or after a successful (non-critical) action.
3. **Make it non-mandatory at first.** Let users skip and re-prompt later. Track adoption.
4. **Mandate for high-value flows.** Once adoption is high, gate sensitive flows (large transfers, treasury actions) behind passkey-only wallets.
5. **Monitor.** Log migration attempts, successes, failures, and PRF-not-supported events so you can size the long tail.

## Errors and edge cases

| Error                                               | Cause                                        | What to do                                                                   |
| --------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------- |
| `Failed to decrypt wallet with provided encryptKey` | User entered the wrong PIN                   | Re-prompt; do not retry the passkey creation step                            |
| WebAuthn aborted / cancelled                        | User dismissed the biometric prompt          | No state changed — safe to retry                                             |
| `Backend rejected wallet encryption update`         | Network or auth failure mid-update           | The backend write is atomic; retry the full migration                        |
| PRF not supported                                   | Device/browser does not support WebAuthn PRF | Keep the user on PIN; surface the migration option only when PRF is detected |

## Related

* [Passkeys guide](/sdk/guides/passkeys) — full passkey + dual-key architecture
* [`useMigrateWalletToPasskey`](/sdk/nextjs/hooks/use-migrate-wallet-to-passkey) — hook reference
* [`useUpdateWalletEncryption`](/sdk/nextjs/hooks/use-update-wallet-encryption) — lower-level encryption update
* [`useChipiSession`](/sdk/nextjs/hooks/use-chipi-session) — session keys with secure server-side storage
