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
WalletClientinterface - 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
| Option | Type | Required | Description |
|---|---|---|---|
accountAddress | Address | Yes | User's smart account address |
chain | Chain | Yes | viem chain configuration |
transport | Transport | Yes | viem transport (e.g., http()) |
providerUrl | string | No | URL of the 1auth provider (defaults to https://passkey.1auth.app) |
dialogUrl | string | No | URL for the dialog UI |
clientId | string | Yes | App metadata identifier; WebAuthn defaults to app-origin |
webauthn | { mode: 'app_origin' | 'experimental_cross_origin'; rpId?: string } | No | WebAuthn 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_signing | boolean | No | Experimental. 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. |
waitForHash | boolean | No | Wait for transaction hash before resolving (default: true) |
hashTimeoutMs | number | No | Maximum time to wait for hash in ms |
hashIntervalMs | number | No | Poll 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
| Property | Type | Required | Description |
|---|---|---|---|
calls | TransactionCall[] | Yes | Array of calls to execute |
chainId | number | No | Override the default chain ID |
tokenRequests | { token: Address; amount: bigint }[] | No | Output tokens to deliver |
TransactionCall
| Property | Type | Required | Description |
|---|---|---|---|
to | Address | Yes | Target contract address |
data | Hex | No | Encoded calldata |
value | bigint | No | ETH value in wei |
label | string | No | Label shown in signing UI |
sublabel | string | No | Additional context for UI |
icon | string | No | Fallback 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. |
abi | readonly unknown[] | No | Optional 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://...ordata: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-1193wallet_sendCallsparams all accept the sameicon?: stringfield — mix and match as you wouldlabelandsublabel.
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…1234Trust 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
toaddress alongside the decode, so ground truth is never hidden. - Silently drops the preview if
decodeFunctionDatathrows — 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
abiis ignored. An app cannot override USDC's verifiedtransferrow with its own ABI definition. - Same field on every call shape.
IntentCall,TransactionCall(walletClient.sendCalls), and the EIP-1193wallet_sendCallsparams all accept the sameabi?: 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:
- Determine required input tokens from your portfolio
- Find the optimal route across chains
- 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 likegetAddresses()work as expected - A configured
clientIddefaults 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: trueshows the 1auth review iframe, then returns approval to the app for app-origin WebAuthn and execution- Use
waitForHash: falseif you only need intent confirmation (faster, but no hash) - Token amounts use
bigintfor viem compatibility - useparseUnits()for decimals