Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Overview

PasskeyWalletClient is a viem-compatible wallet client that extends the standard WalletClient with passkey-authenticated transaction signing. It provides a familiar viem API while handling all WebAuthn operations internally.

Use this when you want:

  • A standard viem WalletClient interface
  • Batched transaction support via sendCalls()
  • Output-first token requests (specify what tokens you want to receive)

Creating a Wallet Client

import { createPasskeyWalletClient } from '@rhinestone/1auth';
import { base } from 'viem/chains';
import { http } from 'viem';
 
const walletClient = createPasskeyWalletClient({
  accountAddress: '0x...', // User's smart account address
  clientId: 'my-dapp',
  experimental_clear_signing: true, // experimental visible 1auth review; WebAuthn stays app-origin
  chain: base,
  transport: http(),
});

Config Options

OptionTypeRequiredDescription
accountAddressAddressYesUser's smart account address
chainChainYesviem chain configuration
transportTransportYesviem transport (e.g., http())
providerUrlstringNoURL of the 1auth provider (defaults to https://passkey.1auth.app)
dialogUrlstringNoURL for the dialog UI
clientIdstringYesApp metadata identifier; WebAuthn defaults to app-origin
webauthn{ mode: 'app_origin' | 'experimental_cross_origin'; rpId?: string }NoWebAuthn placement. Cross-origin WebAuthn is experimental and requires the explicit experimental_cross_origin label. Optionally set rpId for a shared verified app-origin namespace — see App-origin passkeys.
experimental_clear_signingbooleanNoExperimental. Show the 1auth transaction review iframe before signing; the subsequent WebAuthn ceremony still runs on the app origin. Defaults to false; blind signing remains the established flow.
waitForHashbooleanNoWait for transaction hash before resolving (default: true)
hashTimeoutMsnumberNoMaximum time to wait for hash in ms
hashIntervalMsnumberNoPoll interval for hash in ms

With experimental_clear_signing: true, the passkey-origin iframe displays the authoritative server-prepared transaction review. Approval returns to the SDK, then navigator.credentials.get() runs in the application's top-level page using its hostname as the RP ID. The bearer, credential, signer, and smart account stay bound to that origin/RP pair.

Omit the option — the default — only when your app presents the complete transaction details itself. The server still binds and verifies the exact prepared intent, WebAuthn challenge, browser origin, RP ID, account, and credential before submitting anything on-chain. See Signing.

sendCalls

Send one or more calls as a single intent. App-origin WebAuthn runs on the app's top-level origin. Set experimental_clear_signing: true to show the authoritative 1auth review dialog first.

const hash = await walletClient.sendCalls({
  calls: [
    {
      to: '0x...', // Contract address
      data: '0x...', // Encoded calldata
      value: parseEther('0.1'), // Optional ETH value
      label: 'Swap ETH for USDC', // Optional: shown in signing UI
      sublabel: '0.1 ETH → 250 USDC', // Optional: additional context
      icon: 'https://your-app.example/icons/swap.svg', // Optional fallback icon
    },
  ],
  chainId: 8453, // Optional: override chain
});

Parameters

PropertyTypeRequiredDescription
callsTransactionCall[]YesArray of calls to execute
chainIdnumberNoOverride the default chain ID
tokenRequests{ token: Address; amount: bigint }[]NoOutput tokens to deliver

TransactionCall

PropertyTypeRequiredDescription
toAddressYesTarget contract address
dataHexNoEncoded calldata
valuebigintNoETH value in wei
labelstringNoLabel shown in signing UI
sublabelstringNoAdditional context for UI
iconstringNoFallback icon URL (SVG / square PNG / data: URL) shown in the sign dialog when 1auth's built-in token registry can't resolve a logo for this call. See Per-call icons.
abireadonly unknown[]NoOptional ABI used by the sign dialog to render an unverified decode of data (function name + args). See Per-call ABIs.

Returns

Promise<Hash> - The transaction hash once confirmed.

Per-call icons

The sign dialog shows a 48px icon next to every action card. By default 1auth resolves the icon from the token's symbol / address (USDC, ETH, MATIC …) using a built-in registry, and falls back to a neutral letter avatar for unknown contracts.

For demos, custom NFT mints, or any contract 1auth doesn't recognise, pass icon on the call so the dialog renders your glyph instead of the fallback:

await walletClient.sendCalls({
  calls: [
    {
      to: MOCK_NFT_ADDRESS,
      data: encodeFunctionData({ abi: mockNftAbi, functionName: 'mint', args: [price] }),
      label: 'Mint NFT',
      sublabel: '0.10 USDC',
      icon: 'https://your-app.example/nft-glyph.svg',
    },
  ],
  chainId: 421614,
});

Rules

  • Built-in icons always win. If 1auth recognises the call's token (USDC, ETH, …), it uses that icon and ignores icon. This keeps apps from impersonating well-known assets with a custom SVG.
  • Allowed schemes: https://... or data:image/(svg+xml|png|webp|jpeg|gif);.... Other schemes (http:, javascript:, file:, …) are rejected by the server with a 400. Maximum length 8 KB.
  • Same field on every call shape. IntentCall, TransactionCall (walletClient.sendCalls), and the EIP-1193 wallet_sendCalls params all accept the same icon?: string field — mix and match as you would label and sublabel.

Privacy note

HTTPS icon URLs are fetched directly by the user's browser when the sign dialog renders. That leaks the user's IP, User-Agent, and Referer to whatever host you point at — turning the icon URL into a real-time beacon for "user is signing right now." If you don't want to make a third-party request from inside the dialog, use a data:image/svg+xml,… URL instead (the SVG is inlined into the call payload, never fetched).

Per-call ABIs

By default the sign dialog can only decode calls whose contract+selector is in 1auth's ERC-7730 clear-signing registry, or matches the built-in ERC-20 fallback (transfer / approve / transferFrom). Calls to a custom contract — your own marketplace, an NFT mint, a game state update — render as Unverified with just the raw selector.

Pass abi on the call so the dialog can decode data into a human-readable function signature with named arguments:

import { encodeFunctionData } from 'viem';
 
const myAppAbi = [
  {
    name: 'mintWithReferral',
    type: 'function',
    inputs: [
      { name: 'tokenId', type: 'uint256' },
      { name: 'referrer', type: 'address' },
    ],
    outputs: [{ type: 'bool' }],
  },
] as const;
 
await walletClient.sendCalls({
  calls: [
    {
      to: MY_NFT_ADDRESS,
      data: encodeFunctionData({
        abi: myAppAbi,
        functionName: 'mintWithReferral',
        args: [42n, referrerAddress],
      }),
      label: 'Mint NFT',
      sublabel: '0.10 USDC',
      icon: 'https://your-app.example/nft-glyph.svg',
      abi: myAppAbi,
    },
  ],
  chainId: 421614,
});

The sign dialog's expanded action card now shows:

[Unverified]
mintWithReferral(uint256 tokenId, address referrer)
tokenId       42
referrer      0xabcd…1234

Trust model

abi is app-supplied and unverified — the same trust treatment as label, sublabel, and icon. The dialog:

  • Always renders the decoded preview behind an amber "Unverified" badge so users can tell it didn't come from a verified clear-signing entry.
  • Always shows the raw to address alongside the decode, so ground truth is never hidden.
  • Silently drops the preview if decodeFunctionData throws — e.g. when the ABI doesn't include a function with a matching selector. The call still goes through; it just falls back to the unverified-selector display.

This is the same model used by client.grantPermissions({ contracts })'s ABI metadata in the SmartSession review surface.

Rules

  • Verified entries always win. If the call's contract+selector matches a registry entry (ERC-7730 clear-signing data, ERC-20 fallback), that decoded display is used and abi is ignored. An app cannot override USDC's verified transfer row with its own ABI definition.
  • Same field on every call shape. IntentCall, TransactionCall (walletClient.sendCalls), and the EIP-1193 wallet_sendCalls params all accept the same abi?: readonly unknown[] field.
  • Size cap. The server rejects ABIs with more than 256 top-level items to keep prepare-time JSON parsing bounded.

Pairing with viem

The natural pairing is to use the same ABI for both encoding and the preview, so the bytes on the wire and the displayed function are guaranteed to match by construction:

const abi = myAppAbi;
 
await walletClient.sendCalls({
  calls: [
    {
      to: MY_NFT_ADDRESS,
      data: encodeFunctionData({ abi, functionName: 'mint', args: [42n] }),
      abi, // same ABI used for both encoding and display
    },
  ],
});

Token Requests (Output-First Model)

The tokenRequests parameter lets you specify what tokens and amounts you want to receive as the output of your transaction. This is the output-first model - you declare the desired result, and the orchestrator figures out how to achieve it.

import { encodeFunctionData, parseUnits } from 'viem';
 
const hash = await walletClient.sendCalls({
  calls: [
    {
      to: '0xUSDC_ADDRESS',
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: 'transfer',
        args: [recipient, parseUnits('100', 6)],
      }),
      abi: erc20Abi,
      label: 'Send USDC',
    },
  ],
  tokenRequests: [
    {
      token: '0xUSDC_ADDRESS', // Token contract address
      amount: parseUnits('100', 6), // 100 USDC (6 decimals)
    },
  ],
});

Type Signature

import type { Address } from 'viem';
 
tokenRequests?: {
  token: Address;  // ERC-20 token contract address
  amount: bigint;  // Amount in base units (use parseUnits)
}[];

When to Use Token Requests

  • Swaps: Specify the output token and amount, then include any real target calls that consume or settle that output
  • Cross-chain transfers: Declare the destination token and amount
  • Multi-step operations: Let the orchestrator optimize the path

The orchestrator will automatically:

  1. Determine required input tokens from your portfolio
  2. Find the optimal route across chains
  3. Execute bridging and swaps as needed

Example: Token Swap

import { encodeFunctionData, parseUnits } from 'viem';
 
// Swap-funded payment: receive 100 USDC on Base, then transfer it
const hash = await walletClient.sendCalls({
  calls: [
    {
      to: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: 'transfer',
        args: [recipient, parseUnits('100', 6)],
      }),
      abi: erc20Abi,
      label: 'Send USDC',
      sublabel: '100 USDC',
    },
  ],
  tokenRequests: [
    {
      token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
      amount: parseUnits('100', 6),
    },
  ],
  chainId: 8453, // Base
});
 
console.log('Transaction hash:', hash);

Notes

  • The client extends viem's WalletClient, so standard methods like getAddresses() work as expected
  • A configured clientId defaults to app-origin WebAuthn, while the exact page hostname—not registration metadata—selects the credential and account namespace
  • App-origin sendCalls() packages multiple calls into one intent and one passkey ceremony
  • experimental_clear_signing: true shows the 1auth review iframe, then returns approval to the app for app-origin WebAuthn and execution
  • Use waitForHash: false if you only need intent confirmation (faster, but no hash)
  • Token amounts use bigint for viem compatibility - use parseUnits() for decimals