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.

This page walks the modern path — SHHH wallets with a P-256 passkey. The user’s private key lives inside their platform authenticator (Secure Enclave on Apple, TPM on Windows, Android Keystore on Android). It cannot leave the device. The page also points at the legacy PRF path if you’re integrating against existing CHIPI v29 wallets.

1. Register a passkey

createShhhPasskey triggers the browser’s biometric prompt and returns the new credential’s id + public key. Call it from a button click, never on page load.
import { createShhhPasskey } from "@chipi-stack/chipi-passkey";

async function onSignUp(userId: string, userName: string) {
  const { credentialId, publicKeyHex } = await createShhhPasskey(
    userId,
    userName,
  );
  // credentialId  → save with your wallet record
  // publicKeyHex  → { x: "…", y: "…" }, both 32-byte hex strings
  return { credentialId, publicKeyHex };
}
The returned publicKeyHex.x and publicKeyHex.y are the two 32-byte coordinates of the user’s new P-256 key. Pass both into your createWallet call as signerKind: "WEBAUTHN_P256" so the SHHH wallet’s first owner is this passkey.

2. Create a SHHH wallet bound to the passkey

The passkey package never talks to chipi-back directly. Hand the credential to your existing wallet-creation flow — typically useCreateWallet from @chipi-stack/chipi-react — with the right walletType + signerKind:
import { useCreateWallet } from "@chipi-stack/chipi-react";

function SignUpButton() {
  const { createWalletAsync } = useCreateWallet();

  async function onClick() {
    // Step 1 returns these. In a real app you'd carry them across components.
    const { credentialId, publicKeyHex } = await onSignUp(userId, userName);

    const wallet = await createWalletAsync({
      params: {
        externalUserId: userId,
        chain: "STARKNET",
        walletType: "SHHH",
        signerKind: "WEBAUTHN_P256",
        // Pass the freshly-registered passkey pubkey through to the
        // backend SHHH deploy path. The SDK serializes this into the
        // SHHH constructor calldata.
        ownerPubkey: publicKeyHex,
      },
      bearerToken,
    });
    return wallet;
  }
}

3. Sign a transaction with the passkey

signShhhMessage prompts the user with biometrics again, runs the WebAuthn ceremony, and returns the raw signature pieces. The backend SDK accepts these as-is — you don’t need to assemble the SHHH envelope yourself.
import { signShhhMessage } from "@chipi-stack/chipi-passkey";
import { buildWebAuthnEnvelopeFromAssertion } from "@chipi-stack/backend";

async function onSendTransfer(messageHash: bigint, pubkey: { x: Uint8Array; y: Uint8Array }) {
  // 1. Biometric prompt + WebAuthn ceremony.
  const { authenticatorData, clientDataJSON, signatureDer, credentialId } =
    await signShhhMessage({ messageHash });

  // 2. Wrap into the SHHH V2_SNIP12 envelope.
  const envelope = buildWebAuthnEnvelopeFromAssertion({
    authenticatorData,
    clientDataJSON,
    signatureDer,
    pubkey,
    messageHash,
  });

  // 3. Ship the envelope as the `signature` field of your paymaster call.
  return envelope;
}
The wallet’s address never appears in this flow — the verifier reproduces the assertion check on chain using the WEBAUTHN_P256 verifier class registered against the wallet.

Full Next.js / React example

"use client";

import { useState } from "react";
import {
  createShhhPasskey,
  signShhhMessage,
  hasShhhPasskey,
} from "@chipi-stack/chipi-passkey";
import { useCreateWallet } from "@chipi-stack/chipi-react";

export function PasskeySignUp() {
  const { createWalletAsync, isLoading } = useCreateWallet();
  const [wallet, setWallet] = useState<unknown>(null);

  async function onSignUp() {
    const { credentialId, publicKeyHex } = await createShhhPasskey(
      "user-42",
      "Alice",
    );
    const w = await createWalletAsync({
      params: {
        externalUserId: "user-42",
        chain: "STARKNET",
        walletType: "SHHH",
        signerKind: "WEBAUTHN_P256",
        ownerPubkey: publicKeyHex,
      },
      bearerToken,
    });
    setWallet(w);
  }

  async function onSignTx() {
    // Compute the SHHH OE hash for the transfer you want to send — your
    // existing wallet flow already builds this. Replace `0n` with the
    // real `computeOeHash` output for your call.
    const messageHash = 0n;
    const { authenticatorData, clientDataJSON, signatureDer } =
      await signShhhMessage({ messageHash });
    // Forward the assertion bytes to your paymaster flow — see step 3
    // above for the envelope-wrapping detail.
    console.log({ authenticatorData, clientDataJSON, signatureDer });
  }

  if (hasShhhPasskey()) {
    return <button onClick={onSignTx}>Sign a transfer with Touch ID</button>;
  }
  return (
    <button onClick={onSignUp} disabled={isLoading}>
      Sign up with Touch ID
    </button>
  );
}

What happens if the user has a passkey already?

hasShhhPasskey() checks localStorage for a stored credential id, so you can branch your UI between “Sign up” and “Sign in” without prompting. If the user wipes their browser storage, the credential id is lost but the passkey itself still lives in the platform authenticator — you’ll prompt them once to re-discover it.

When biometrics aren’t available

If isWebAuthnSupported() returns false, fall back to the PIN flow described in When passkeys fail. That flow is also the right one for legacy CHIPI v29 wallets, which use a PRF-derived encryption key instead of a P-256 signature.