Skip to main content
The Passkey signer uses the WebAuthn Validator module to authorise transactions with device-native biometrics: Face ID, Touch ID, Windows Hello. No seed phrases, no browser extensions. Keys are generated and stored in the device secure enclave and can sync across devices via iCloud Keychain or Google Password Manager. Passkeys are supported across iOS, Android, macOS, and modern browsers (Chrome, Safari, Edge).

Single owner

import { RhinestoneSDK } from '@rhinestone/sdk'
import { toWebAuthnAccount } from 'viem/account-abstraction'

const passkeyAccount = toWebAuthnAccount({ credential })

const rhinestone = new RhinestoneSDK({
  apiKey: process.env.RHINESTONE_API_KEY as string,
})

const account = await rhinestone.createAccount({
  owners: {
    type: 'passkey',
    accounts: [passkeyAccount],
  },
})

Multisig

The WebAuthn Validator supports n/m multisig with multiple passkey owners and a configurable signature threshold. A common use case: users register a passkey per device. You set up a 1-of-n account so the user can sign from any of their devices without needing to migrate keys.

Setup

const account = await rhinestone.createAccount({
  owners: {
    type: 'passkey',
    accounts: [passkeyAccountA, passkeyAccountB],
    threshold: 2, // 2-of-2: both devices must sign
  },
})
By default, threshold is 1, meaning any single passkey can sign.

Signing with a subset of owners

For m-of-n setups, specify which passkeys to use when sending a transaction:
const transaction = await account.prepareTransaction({
  chain,
  calls: [
    // …
  ],
  signers: {
    type: 'owner',
    kind: 'passkey',
    accounts: [passkeyAccountA],
  },
})
Provide at least as many signers as the threshold requires, or the transaction will fail validation.

Add a passkey (new device)

import { addOwner } from '@rhinestone/sdk/actions/passkeys'
import { slice } from 'viem'

const pubKeyX = BigInt(slice(newPasskeyAccount.publicKey, 0, 32))
const pubKeyY = BigInt(slice(newPasskeyAccount.publicKey, 32))
const requiresUV = false

const transaction = await account.prepareTransaction({
  chain,
  calls: [addOwner(pubKeyX, pubKeyY, requiresUV)],
})

Remove a passkey

import { removeOwner } from '@rhinestone/sdk/actions/passkeys'

const transaction = await account.prepareTransaction({
  chain,
  calls: [removeOwner(pubKeyX, pubKeyY)],
})

Change threshold

import { changeThreshold } from '@rhinestone/sdk/actions/passkeys'

const transaction = await account.prepareTransaction({
  chain,
  calls: [changeThreshold(2)],
})

Get current owners

const owners = await account.getOwners(chain)
// → { accounts: ['0xaaa…', '0xbbb…'], threshold: 1 }

Enable in a separate transaction

If you need to enable the passkey module on an existing account rather than at creation time:
import { enable as enablePasskeys } from '@rhinestone/sdk/actions/passkeys'

const transaction = await account.prepareTransaction({
  chain,
  calls: [enablePasskeys(passkeyAccount.publicKey, passkeyAccount.id)],
  tokenRequests: [],
})

Example: multi-device 1-of-n

Register a passkey per device and use a 1-of-n threshold, so the user can sign from any of their devices.
import { RhinestoneSDK } from '@rhinestone/sdk'
import { toWebAuthnAccount } from 'viem/account-abstraction'
import { base, arbitrum } from 'viem/chains'
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem'

// One passkey per device, each registered via WebAuthn on that device
const passkeyAccountA = toWebAuthnAccount({ credential: credentialFromDeviceA })
const passkeyAccountB = toWebAuthnAccount({ credential: credentialFromDeviceB })

const rhinestone = new RhinestoneSDK({
  apiKey: process.env.RHINESTONE_API_KEY as string,
})

// Any single registered device can sign
const account = await rhinestone.createAccount({
  owners: {
    type: 'passkey',
    accounts: [passkeyAccountA, passkeyAccountB],
    threshold: 1,
  },
})

const usdcAmount = parseUnits('0.1', 6)
const prepared = await account.prepareTransaction({
  sourceChains: [base],
  targetChain: arbitrum,
  calls: [
    {
      to: 'USDC',
      value: 0n,
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: 'transfer',
        args: ['0xd8da6bf26964af9d7eed9e03e53415d37aa96045', usdcAmount],
      }),
    },
  ],
  tokenRequests: [{ address: 'USDC', amount: usdcAmount }],
  // Sign from a single device
  signers: {
    type: 'owner',
    kind: 'passkey',
    accounts: [passkeyAccountA],
  },
})

const signed = await account.signTransaction(prepared)
const transaction = await account.submitTransaction(signed)
const result = await account.waitForExecution(transaction)
When the user adds a new device, register a passkey for it and add it as an owner — see Add a passkey (new device). See it in action: passkey example — a passkey-only account with no third-party auth provider.