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.
Viem-style PublicClient
This Skill demonstrates how to build a viem-compatible PublicClient using Voltaire primitives. The implementation follows viem’s architecture patterns while leveraging Voltaire’s type-safe primitives.
Overview
The PublicClient provides access to public Ethereum JSON-RPC methods like fetching blocks, transactions, balances, and making calls. Key features:
- Transport abstraction - HTTP, WebSocket, or custom transports
- Extend pattern - Composable client extension for custom actions
- Type safety - Full TypeScript types for all methods
- Caching - Built-in request caching with configurable TTL
Quick Start
import { createPublicClient, http, mainnet } from './examples/viem-publicclient/index.js';
const client = createPublicClient({
chain: mainnet,
transport: http('https://eth.example.com')
});
// Get block number
const blockNumber = await client.getBlockNumber();
// => 19123456n
// Get balance
const balance = await client.getBalance({
address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0'
});
// => 1000000000000000000n (1 ETH)
// Make a call
const result = await client.call({
to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
data: '0x70a08231...'
});
Architecture
Client Structure
PublicClient
├── Base Properties
│ ├── chain - Chain configuration
│ ├── transport - Transport config + request function
│ ├── cacheTime - Cache duration (ms)
│ ├── pollingInterval - Block polling interval
│ └── uid - Unique client identifier
├── Public Actions
│ ├── getBlockNumber()
│ ├── getBalance()
│ ├── getBlock()
│ ├── call()
│ ├── estimateGas()
│ ├── getTransaction()
│ ├── getTransactionReceipt()
│ ├── getLogs()
│ └── ...
└── extend() - Composable extension
Transport Layer
The transport layer handles JSON-RPC communication:
import { http } from './examples/viem-publicclient/index.js';
// HTTP transport with custom config
const transport = http('https://eth.example.com', {
timeout: 10_000,
retryCount: 3,
retryDelay: 150,
fetchOptions: {
headers: { 'Authorization': 'Bearer ...' }
}
});
const client = createPublicClient({
chain: mainnet,
transport
});
Extend Pattern
Extend the client with custom actions:
const client = createPublicClient({
chain: mainnet,
transport: http()
});
// Add custom actions
const extendedClient = client.extend((base) => ({
async getDoubleBlockNumber() {
const blockNumber = await base.getBlockNumber();
return blockNumber * 2n;
},
async getFormattedBalance(address: string) {
const balance = await base.getBalance({ address });
return `${balance / 10n ** 18n} ETH`;
}
}));
// Use custom actions
const doubled = await extendedClient.getDoubleBlockNumber();
const formatted = await extendedClient.getFormattedBalance('0x...');
Chain extensions:
const client = createPublicClient({
chain: mainnet,
transport: http()
})
.extend((base) => ({
action1: () => base.getBlockNumber()
}))
.extend((base) => ({
action2: () => base.action1() // Can use previous extensions
}));
API Reference
createPublicClient
Creates a PublicClient with public actions.
function createPublicClient(config: PublicClientConfig): PublicClient
Parameters:
| Name | Type | Description |
|---|
| chain | Chain | Chain configuration (optional) |
| transport | TransportFactory | Transport factory (required) |
| cacheTime | number | Cache duration in ms (default: pollingInterval) |
| pollingInterval | number | Block polling interval (default: blockTime/2) |
| key | string | Client key (default: ‘public’) |
| name | string | Client name (default: ‘Public Client’) |
Public Actions
getBlockNumber
Returns the current block number.
const blockNumber = await client.getBlockNumber();
// => 19123456n
getBalance
Returns the balance of an address.
const balance = await client.getBalance({
address: '0x...',
blockTag: 'latest' // or blockNumber: 19000000n
});
// => 1000000000000000000n
getBlock
Returns block information.
const block = await client.getBlock({
blockNumber: 19000000n,
// or blockHash: '0x...',
includeTransactions: true
});
call
Executes a call without creating a transaction.
const result = await client.call({
to: '0x...',
data: '0x...',
value: 0n,
blockTag: 'latest'
});
// => { data: '0x...' }
estimateGas
Estimates gas for a transaction.
const gas = await client.estimateGas({
to: '0x...',
value: 1000000000000000000n
});
// => 21000n
getTransaction
Returns transaction by hash.
const tx = await client.getTransaction({
hash: '0x...'
});
getTransactionReceipt
Returns transaction receipt.
const receipt = await client.getTransactionReceipt({
hash: '0x...'
});
console.log(receipt.status); // 'success' | 'reverted'
getLogs
Returns logs matching filter.
const logs = await client.getLogs({
address: '0x...',
fromBlock: 19000000n,
toBlock: 19000100n,
topics: ['0xddf252ad...']
});
getCode
Returns contract bytecode.
const code = await client.getCode({
address: '0x...'
});
getStorageAt
Returns storage at slot.
const value = await client.getStorageAt({
address: '0x...',
slot: '0x0'
});
getTransactionCount
Returns transaction count (nonce).
const nonce = await client.getTransactionCount({
address: '0x...'
});
getChainId
Returns chain ID.
const chainId = await client.getChainId();
// => 1
getGasPrice
Returns current gas price.
const gasPrice = await client.getGasPrice();
// => 20000000000n (20 gwei)
Chain Definitions
Pre-configured chain definitions:
import { mainnet, sepolia, optimism, arbitrum, polygon, base } from './chains.js';
const client = createPublicClient({
chain: mainnet, // Uses default RPC
transport: http()
});
Create custom chains:
const myChain = {
id: 12345,
name: 'My Chain',
nativeCurrency: {
name: 'Ether',
symbol: 'ETH',
decimals: 18
},
rpcUrls: {
default: {
http: ['https://rpc.mychain.com']
}
},
blockTime: 2_000,
blockExplorers: {
default: {
name: 'Explorer',
url: 'https://explorer.mychain.com'
}
}
};
Error Handling
The client throws typed errors:
import {
RpcRequestError,
TransactionNotFoundError,
BlockNotFoundError,
UrlRequiredError
} from './errors.js';
try {
const tx = await client.getTransaction({ hash: '0x...' });
} catch (error) {
if (error instanceof TransactionNotFoundError) {
console.log('Transaction not found:', error.details.hash);
} else if (error instanceof RpcRequestError) {
console.log('RPC error:', error.details.error.message);
}
}
Testing
Run the test suite:
cd examples/viem-publicclient
npx vitest run
Implementation Notes
Caching
Block number is cached to reduce RPC calls:
// First call hits RPC
await client.getBlockNumber();
// Second call returns cached value (within cacheTime)
await client.getBlockNumber();
Retry Logic
HTTP transport automatically retries on network errors (not RPC errors):
const transport = http('https://eth.example.com', {
retryCount: 3, // Max retries
retryDelay: 150 // Delay between retries (ms)
});
Polling Interval
Polling interval is derived from chain block time:
- Mainnet (12s blocks): 4000ms polling
- Arbitrum (250ms blocks): 500ms polling
File Structure
examples/viem-publicclient/
├── REQUIREMENTS.md # Requirements from viem analysis
├── index.ts # Main exports
├── PublicClientType.ts # Type definitions
├── createPublicClient.js # Client factory
├── createClient.js # Base client factory
├── createTransport.js # Transport factory
├── http.js # HTTP transport
├── publicActions.js # Actions decorator
├── chains.js # Chain definitions
├── errors.ts # Error types
├── actions/ # Action implementations
│ ├── index.ts
│ ├── getBlockNumber.js
│ ├── getBalance.js
│ ├── getChainId.js
│ ├── call.js
│ ├── getBlock.js
│ ├── estimateGas.js
│ ├── getTransaction.js
│ ├── getTransactionReceipt.js
│ ├── getLogs.js
│ ├── getCode.js
│ ├── getStorageAt.js
│ ├── getTransactionCount.js
│ └── getGasPrice.js
├── utils/
│ ├── uid.js # Unique ID generator
│ ├── cache.js # Caching utilities
│ └── encoding.js # Hex encoding utilities
├── PublicClient.test.ts # Tests
└── vitest.config.ts # Test config
Next Steps
Future enhancements:
- readContract - ABI-aware contract reads
- multicall - Batched contract calls
- watchBlockNumber - Block number subscription
- watchBlocks - Block subscription
- WebSocket transport - Real-time subscriptions
- ENS support - Name resolution