Skip to main content
v14.7.0 completes external-rooted SHHH signing. A SHHH wallet’s first owner can now be a passkey (WEBAUTHN_P256) or a MetaMask key (EIP191_SECP256K1) at createWallet time — Chipi holds no key — and a Phantom wallet (ED25519) can co-sign an OutsideExecution from the signature its signMessage returns. Additive only: existing STARK and Chipi-held paths are unchanged.

TL;DR — does this affect me?

Your setupAction you need to take
Pinned to @chipi-stack/backend@14.6.x (or earlier)None. These are additive APIs; nothing existing changes.
You create SHHH wallets with signerKind: "STARK" (or omit it)None. The Chipi-held STARK path is untouched.
You want a passkey / MetaMask to be the wallet’s first ownerPass signerKind: "WEBAUTHN_P256" / "EIP191_SECP256K1" + ownerPubkeyFelts to createWallet (see below).
You onboard Phantom signers into a threshold walletUse the new buildEd25519EnvelopeFromSignature to co-sign.

External-rooted createWallet (passkey, MetaMask)

Before v14.7.0, createWallet shipped only STARK and ED25519 and always expected a key Chipi could hold or generate. Now WEBAUTHN_P256 and EIP191_SECP256K1 deploy a SHHH wallet whose first owner is an external key: you supply the owner’s pubkey felts, and no encryptedPrivateKey is ever created.
// Passkey-as-owner (zero Chipi-held key). The 4 felts come from the
// passkey's P-256 pubkey — e.g. via createShhhPasskey + the pubkey helpers.
const wallet = await sdk.createWallet({
  params: {
    externalUserId: "treasury-owner-1",
    chain: Chain.STARKNET,
    walletType: "SHHH",
    signerKind: "WEBAUTHN_P256",
    ownerPubkeyFelts, // [x_low, x_high, y_low, y_high]
  },
});
// wallet.signerKind === "WEBAUTHN_P256"
// wallet.encryptedPrivateKey === null   ← Chipi holds nothing
The same shape works for signerKind: "EIP191_SECP256K1" (MetaMask). The create payload sends ownerPublicKey (not encryptedPrivateKey) as the credential source; the wallet address is derived from ownerPubkeyFelts. JWT_ES256_APPLE_SUB is still gated — it needs an Apple-Developer context.

Phantom co-sign — buildEd25519EnvelopeFromSignature

Phantom (and any external Ed25519 / Solana wallet) never exposes its private key, so buildEd25519Envelope — which requires the raw key — couldn’t be used. The new from-signature builder wraps the 64-byte signature Phantom returns into a V2_SNIP12 envelope, mirroring buildEip191EnvelopeFromSignature for MetaMask.
import {
  buildEd25519EnvelopeFromSignature,
  ed25519SignedBytes,
} from "@chipi-stack/backend";

await window.solana.connect();
const publicKey = window.solana.publicKey.toBytes();      // 32-byte pubkey

// Sign the EXACT bytes the verifier reconstructs — NOT the raw 32-byte hash.
const msg = ed25519SignedBytes(oeHash);
const { signature } = await window.solana.signMessage(msg); // 64-byte R‖s

const envelope = await buildEd25519EnvelopeFromSignature({
  signature,
  publicKey,
  messageHash: oeHash,
});
Two notes that matter:
  • It’s async. Unlike the synchronous EIP-191 builder, the Ed25519 path initializes the Garaga WASM hint builder on first call. await it.
  • Sign the encoded bytes. The wallet must sign ed25519SignedBytes(messageHash) (the 64-byte hex-ASCII encoding), not the raw hash — the on-chain verifier reconstructs those exact bytes.
See Threshold & co-signing for using this in an N-of-M wallet.