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
username: 'user@example.com',
chain: base,
transport: http(),
});Config Options
| Option | Type | Required | Description |
|---|---|---|---|
accountAddress | Address | Yes | User's smart account address |
username | string | Yes | Username for the passkey provider |
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.box) |
dialogUrl | string | No | URL for the dialog UI |
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 |
sendCalls
Send multiple calls as a single batched transaction. Opens the passkey modal for user approval.
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: string; 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 { parseUnits } from 'viem';
const hash = await walletClient.sendCalls({
calls: [
{
to: '0xUSDC_ADDRESS',
data: '0x', // Empty for simple transfers
label: 'Receive USDC',
},
],
tokenRequests: [
{
token: '0xUSDC_ADDRESS', // Token contract address
amount: parseUnits('100', 6), // 100 USDC (6 decimals)
},
],
});Type Signature
tokenRequests?: {
token: string; // 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 you want to receive
- 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 { parseUnits } from 'viem';
// Swap: receive 100 USDC on Base
const hash = await walletClient.sendCalls({
calls: [
{
to: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
data: '0x',
label: 'Swap to USDC',
sublabel: 'Receive 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 sendCalls()opens the passkey modal for user approval before executing- Use
waitForHash: falseif you only need intent confirmation (faster, but no hash) - Token amounts use
bigintfor viem compatibility - useparseUnits()for decimals