Documentation Index
Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
Use this file to discover all available pages before exploring further.
Skill — Copyable reference implementation. Use as-is or customize. See Skills Philosophy.
WalletClient Implementation
This Skill documents how to implement a viem-compatible WalletClient using Voltaire primitives. The implementation provides a drop-in replacement for viem’s wallet functionality.
Quick Start
import { createWalletClient, http, custom } from './examples/viem-walletclient/index.js';
import { privateKeyToAccount } from './examples/viem-account/index.js';
// Local account (signing locally)
const account = privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80');
const client = createWalletClient({
account,
chain: mainnet,
transport: http('https://mainnet.infura.io/v3/YOUR_KEY'),
});
// Send transaction
const hash = await client.sendTransaction({
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
value: 1000000000000000000n,
});
// Sign message
const signature = await client.signMessage({
message: 'Hello, Ethereum!',
});
Account Types
Local Account
Local accounts sign transactions and messages locally using a private key. The signed data is then sent to the network via eth_sendRawTransaction.
import { privateKeyToAccount } from './examples/viem-account/index.js';
const account = privateKeyToAccount('0x...');
const client = createWalletClient({
account,
chain: mainnet,
transport: http('https://...'),
});
// Account is "hoisted" - no need to specify per action
const hash = await client.sendTransaction({
to: '0x...',
value: 1n,
});
JSON-RPC Account
JSON-RPC accounts delegate signing to an external wallet (like MetaMask). The wallet handles signing via eth_sendTransaction, personal_sign, etc.
// Browser: using injected wallet
const client = createWalletClient({
transport: custom(window.ethereum),
});
// Request wallet connection
const addresses = await client.requestAddresses();
// Wallet will prompt user to sign
const hash = await client.sendTransaction({
account: addresses[0],
to: '0x...',
value: 1n,
});
Wallet Actions
sendTransaction
Creates, signs, and sends a transaction to the network.
// Local account: signs locally, sends via eth_sendRawTransaction
const hash = await client.sendTransaction({
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
value: 1000000000000000000n, // 1 ETH
gas: 21000n, // Optional
maxFeePerGas: 20000000000n, // Optional (EIP-1559)
});
// JSON-RPC account: calls eth_sendTransaction
const hash = await client.sendTransaction({
account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
to: '0x...',
value: 1n,
});
signMessage
Signs a message using EIP-191 format.
// String message
const signature = await client.signMessage({
message: 'Hello, Ethereum!',
});
// Raw bytes
const signature = await client.signMessage({
message: { raw: new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]) },
});
// Raw hex
const signature = await client.signMessage({
message: { raw: '0x48656c6c6f' },
});
signTypedData
Signs EIP-712 typed data.
const signature = await client.signTypedData({
domain: {
name: 'Ether Mail',
version: '1',
chainId: 1n,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
types: {
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' },
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'string' },
],
},
primaryType: 'Mail',
message: {
from: { name: 'Alice', wallet: '0x...' },
to: { name: 'Bob', wallet: '0x...' },
contents: 'Hello!',
},
});
signTransaction
Signs a transaction without broadcasting it.
const signedTx = await client.signTransaction({
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
value: 1000000000000000000n,
});
// Later: send the signed transaction
const hash = await client.sendRawTransaction({
serializedTransaction: signedTx,
});
getAddresses / requestAddresses
// Get addresses without prompting (eth_accounts)
const addresses = await client.getAddresses();
// Request addresses with wallet prompt (eth_requestAccounts)
const addresses = await client.requestAddresses();
Extend Pattern
Add custom actions to the client:
const client = createWalletClient({
account,
chain: mainnet,
transport: http(),
}).extend((base) => ({
// Custom action using base client
sendEth: async (to: string, amount: bigint) => {
return base.sendTransaction({ to, value: amount });
},
// Access chain info
getChainName: () => base.chain?.name ?? 'Unknown',
}));
// Use custom actions
await client.sendEth('0x...', 1n);
console.log(client.getChainName()); // 'Ethereum'
Transports
HTTP Transport
import { http } from './examples/viem-walletclient/index.js';
const client = createWalletClient({
transport: http('https://mainnet.infura.io/v3/YOUR_KEY', {
timeout: 10_000, // Request timeout (ms)
retryCount: 3, // Number of retries
retryDelay: 150, // Delay between retries (ms)
}),
});
Custom Transport (EIP-1193 Provider)
import { custom } from './examples/viem-walletclient/index.js';
// Browser wallet
const client = createWalletClient({
transport: custom(window.ethereum),
});
// Or any EIP-1193 compatible provider
const client = createWalletClient({
transport: custom(myProvider),
});
Fallback Transport
import { fallback, http } from './examples/viem-walletclient/index.js';
const client = createWalletClient({
transport: fallback([
http('https://mainnet.infura.io/v3/...'),
http('https://eth-mainnet.alchemyapi.io/v2/...'),
http('https://cloudflare-eth.com'),
]),
});
Chain Configuration
const mainnet = {
id: 1,
name: 'Ethereum',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: {
default: { http: ['https://eth.llamarpc.com'] },
},
blockExplorers: {
default: { name: 'Etherscan', url: 'https://etherscan.io' },
},
blockTime: 12_000, // Block time in ms (affects polling interval)
};
const client = createWalletClient({
chain: mainnet,
transport: http(),
});
Error Handling
import {
AccountNotFoundError,
ChainMismatchError,
TransactionExecutionError,
} from './examples/viem-walletclient/index.js';
try {
await client.sendTransaction({ to: '0x...', value: 1n });
} catch (error) {
if (error instanceof AccountNotFoundError) {
console.error('No account configured');
} else if (error instanceof ChainMismatchError) {
console.error(`Expected chain ${error.expectedChainId}, got ${error.currentChainId}`);
} else if (error instanceof TransactionExecutionError) {
console.error('Transaction failed:', error.message);
}
}
Integration with PublicClient
WalletClient is typically used alongside PublicClient for read operations:
import { createPublicClient, http } from 'viem';
import { createWalletClient } from './examples/viem-walletclient/index.js';
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
const walletClient = createWalletClient({
account: privateKeyToAccount('0x...'),
chain: mainnet,
transport: http(),
});
// Read: use public client
const balance = await publicClient.getBalance({ address: account.address });
// Write: use wallet client
const hash = await walletClient.sendTransaction({
to: '0x...',
value: balance / 2n,
});
// Wait for confirmation: use public client
const receipt = await publicClient.waitForTransactionReceipt({ hash });
File Structure
examples/viem-walletclient/
├── REQUIREMENTS.md # Extracted viem patterns
├── WalletClientTypes.ts # Type definitions
├── errors.ts # Custom error classes
├── createWalletClient.js # Main factory function
├── getAddresses.js # eth_accounts action
├── requestAddresses.js # eth_requestAccounts action
├── sendTransaction.js # Send transaction action
├── signMessage.js # Sign message action
├── signTypedData.js # Sign typed data action
├── signTransaction.js # Sign transaction action
├── getChainId.js # Get chain ID action
├── prepareTransactionRequest.js # Prepare transaction
├── sendRawTransaction.js # Send raw transaction
├── transports.js # Transport implementations
├── index.ts # Module exports
└── WalletClient.test.ts # Tests
Implementation Notes
- Account Hoisting: Account can be set on client creation, automatically used by all actions
- parseAccount: Converts string address to JSON-RPC account object
- Chain Validation: Validates connected chain matches expected chain before sending
- Fee Estimation: Automatically estimates EIP-1559 fees if not provided
- Transaction Preparation: Fills nonce, gas, fees automatically
- Extend Pattern: Allows adding custom actions while preserving base functionality