viem Adapter
Use SpendSafe with viem, the modern TypeScript alternative to ethers.js.
Overview
Best for: Modern TypeScript projects, small bundle sizes, functional programming style Chains: All EVM-compatible blockchains Bundle size: ~35kB (88% smaller than ethers.js) Performance: Fast tree-shaking, optimised for modern bundlers
Installation
npm install @spendsafe/sdk viem
Peer dependencies: viem v2.x
Basic Setup
1. Import the adapter
import { PolicyWallet, createViemAdapter } from '@spendsafe/sdk';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
2. Create the adapter
const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const rpcUrl = process.env.RPC_URL;
const adapter = await createViemAdapter(privateKey, rpcUrl, mainnet);
3. Wrap with PolicyWallet
const wallet = new PolicyWallet(adapter, {
apiKey: process.env.SPENDSAFE_API_KEY,
});
4. Send transactions
// Native ETH transfer
await wallet.send({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '1000000000000000000', // 1 ETH in wei
});
// ERC-20 token transfer (e.g., USDC)
await wallet.send({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '1000000', // 1 USDC (6 decimals)
asset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC contract
});
Configuration
Environment Variables
# Required
PRIVATE_KEY=0x... # Your wallet private key (with 0x prefix)
RPC_URL=https://... # Your RPC endpoint
SPENDSAFE_API_KEY=sk_... # Your SpendSafe API key
Chain Configuration
viem uses chain objects for network configuration:
import { mainnet, sepolia, base, baseSepolia, polygon } from 'viem/chains';
// Ethereum Mainnet
const adapter = await createViemAdapter(privateKey, rpcUrl, mainnet);
// Ethereum Sepolia (testnet)
const adapter = await createViemAdapter(privateKey, rpcUrl, sepolia);
// Base Mainnet
const adapter = await createViemAdapter(privateKey, rpcUrl, base);
// Base Sepolia
const adapter = await createViemAdapter(privateKey, rpcUrl, baseSepolia);
// Polygon Mainnet
const adapter = await createViemAdapter(privateKey, rpcUrl, polygon);
Custom Chains
Define custom EVM chains:
import { defineChain } from 'viem';
const myCustomChain = defineChain({
id: 12345,
name: 'My Custom Chain',
nativeCurrency: {
decimals: 18,
name: 'Ether',
symbol: 'ETH',
},
rpcUrls: {
default: {
http: ['https://rpc.mycustomchain.com'],
},
},
blockExplorers: {
default: { name: 'Explorer', url: 'https://explorer.mycustomchain.com' },
},
});
const adapter = await createViemAdapter(privateKey, rpcUrl, myCustomChain);
Complete Example
import { PolicyWallet, createViemAdapter } from '@spendsafe/sdk';
import { mainnet } from 'viem/chains';
import * as dotenv from 'dotenv';
dotenv.config();
async function main() {
// 1. Create viem adapter
const adapter = await createViemAdapter(
process.env.PRIVATE_KEY as `0x${string}`,
process.env.RPC_URL!,
mainnet
);
// 2. Create PolicyWallet
const wallet = new PolicyWallet(adapter, {
apiKey: process.env.SPENDSAFE_API_KEY!,
});
try {
// 3. Send ETH with policy enforcement
const result = await wallet.send({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '1000000000000000000', // 1 ETH
});
console.log('✅ Transaction sent:', result.hash);
console.log('⛽ Gas fee:', result.fee);
} catch (error) {
if (error.code === 'POLICY_VIOLATION') {
console.log('❌ Policy violation:', error.message);
console.log(' Reason:', error.details.reason);
console.log(' Remaining today:', error.details.remainingDaily);
} else {
throw error;
}
}
}
main();
Advanced Usage
ERC-20 Token Transfers
// USDC transfer (6 decimals)
await wallet.send({
to: recipientAddress,
amount: '1000000', // 1 USDC
asset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
});
// DAI transfer (18 decimals)
await wallet.send({
to: recipientAddress,
amount: '1000000000000000000', // 1 DAI
asset: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI on Ethereum
});
Multiple Chains
import { mainnet, base } from 'viem/chains';
// Ethereum mainnet wallet
const ethAdapter = await createViemAdapter(privateKey, ethRpcUrl, mainnet);
const ethWallet = new PolicyWallet(ethAdapter, { apiKey: 'key-1' });
// Base mainnet wallet
const baseAdapter = await createViemAdapter(privateKey, baseRpcUrl, base);
const baseWallet = new PolicyWallet(baseAdapter, { apiKey: 'key-2' });
// Send on different chains
await ethWallet.send({ to: '0x...', amount: '1000000' });
await baseWallet.send({ to: '0x...', amount: '1000000' });
Error Handling
import { PolicyError } from '@spendsafe/sdk';
try {
await wallet.send({ to: merchant, amount: '1000000' });
} catch (error) {
if (error instanceof PolicyError) {
// Policy violation
console.log('Policy blocked transaction:');
console.log('- Reason:', error.details.reason);
console.log('- Daily limit:', error.details.limits.dailyLimit);
console.log('- Remaining:', error.details.counters.remainingDaily);
} else if (error.name === 'InsufficientFundsError') {
// Not enough balance (viem error type)
console.log('Insufficient funds');
} else {
// Other error (network, gas, etc.)
console.error('Transaction failed:', error.message);
}
}
Type Safety with viem
viem provides excellent TypeScript support:
import type { Address, Hash } from 'viem';
// Type-safe addresses
const recipient: Address = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1';
// Send with type safety
const result = await wallet.send({
to: recipient,
amount: '1000000',
});
// Result is typed
const txHash: Hash = result.hash;
Balance Checks
// Get wallet balance
const balance = await adapter.getBalance();
console.log('Wallet balance:', balance, 'wei');
// viem utilities for formatting
import { formatEther } from 'viem';
const balanceEth = formatEther(BigInt(balance));
console.log('Balance:', balanceEth, 'ETH');
Gas Estimation
// Adapter handles gas estimation automatically
const result = await wallet.send({
to: '0x...',
amount: '1000000',
// Gas estimated automatically using viem's estimateGas
});
console.log('Gas fee paid:', result.fee); // in ETH
Network Configuration
Ethereum Networks
import { mainnet, sepolia } from 'viem/chains';
// Mainnet
const adapter = await createViemAdapter(privateKey, rpcUrl, mainnet);
// Sepolia testnet
const adapter = await createViemAdapter(privateKey, rpcUrl, sepolia);
Base Networks
import { base, baseSepolia } from 'viem/chains';
// Base Mainnet
const adapter = await createViemAdapter(privateKey, rpcUrl, base);
// Base Sepolia
const adapter = await createViemAdapter(privateKey, rpcUrl, baseSepolia);
Other EVM Networks
import { polygon, optimism, arbitrum, avalanche } from 'viem/chains';
// Polygon
const adapter = await createViemAdapter(privateKey, rpcUrl, polygon);
// Optimism
const adapter = await createViemAdapter(privateKey, rpcUrl, optimism);
// Arbitrum
const adapter = await createViemAdapter(privateKey, rpcUrl, arbitrum);
// Avalanche
const adapter = await createViemAdapter(privateKey, rpcUrl, avalanche);
Troubleshooting
"Invalid private key format" error
Problem: Private key doesn't match TypeScript type
Solution: Ensure private key is typed as hex string:
// ✅ Correct
const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
// ❌ Wrong
const privateKey = process.env.PRIVATE_KEY; // Type error
"Chain not supported" error
Problem: Chain object not imported or defined
Solution: Import chain from viem/chains or define custom:
// ✅ Correct
import { mainnet } from 'viem/chains';
const adapter = await createViemAdapter(pk, rpc, mainnet);
// ❌ Wrong
const adapter = await createViemAdapter(pk, rpc, 'mainnet');
"Insufficient funds" error
Problem: Wallet doesn't have enough ETH for transaction + gas
Solution: Fund your wallet with ETH for gas fees:
// Check balance first
const balance = await adapter.getBalance();
console.log('Balance (wei):', balance);
// Ensure balance > amount + gas fees
import { formatEther } from 'viem';
console.log('Balance (ETH):', formatEther(BigInt(balance)));
"RPC request failed" error
Problem: RPC endpoint is down or rate limited
Solution: Use reliable RPC provider (Alchemy, Infura):
// ✅ Good - Reliable provider
const rpcUrl = 'https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY';
// ❌ Risky - Public RPC (rate limits)
const rpcUrl = 'https://eth.public-rpc.com';
Type errors with amounts
Problem: Amount type mismatch
Solution: Use string type for amounts:
// ✅ Correct
await wallet.send({
to: '0x...',
amount: '1000000', // String
});
// ❌ Wrong
await wallet.send({
to: '0x...',
amount: 1000000, // Number - type error
});
Best Practises
1. Use viem's type system
import type { Address, Hash, Hex } from 'viem';
// Type-safe development
const recipient: Address = '0x...';
const privateKey: Hex = '0x...';
2. Leverage viem utilities
import { formatEther, parseEther, formatUnits, parseUnits } from 'viem';
// Format amounts for display
const ethAmount = formatEther(1000000000000000000n); // "1"
// Parse user input to wei
const weiAmount = parseEther('1.5'); // 1500000000000000000n
// Handle custom decimals (e.g., USDC with 6 decimals)
const usdcAmount = parseUnits('10.5', 6); // 10500000n
3. Use testnet for development
import { sepolia } from 'viem/chains';
// Development: Use Sepolia testnet
const adapter = await createViemAdapter(privateKey, rpcUrl, sepolia);
// Production: Use mainnet
import { mainnet } from 'viem/chains';
const adapter = await createViemAdapter(privateKey, rpcUrl, mainnet);
4. Handle errors gracefully
import { PolicyError } from '@spendsafe/sdk';
import { BaseError } from 'viem';
try {
await wallet.send({ to, amount });
} catch (error) {
if (error instanceof PolicyError) {
// Policy violation
await notifyAdmin(`Policy blocked: ${error.details.reason}`);
} else if (error instanceof BaseError) {
// viem error (network, gas, etc.)
console.error('viem error:', error.shortMessage);
}
}
5. Use environment variables
// ✅ Good
const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
// ❌ Never hardcode
const privateKey = '0xabc123...' as `0x${string}`; // DON'T DO THIS
Why Choose viem?
Bundle Size Comparison
- ethers.js v6: ~300kB
- viem: ~35kB (88% smaller)
- Impact: Faster page loads, better mobile performance
TypeScript-First Design
// viem provides full type safety
import type { Address, Hash, TransactionReceipt } from 'viem';
// No type casting needed
const address: Address = await adapter.getAddress();
const balance: bigint = BigInt(await adapter.getBalance());
Modern API Design
// Functional, composable, tree-shakeable
import { formatEther, parseEther } from 'viem';
// vs ethers.js object-oriented style
// import { ethers } from 'ethers';
// ethers.utils.formatEther(...)
Performance
- Better tree-shaking (smaller production bundles)
- Optimised for modern bundlers (Vite, Rollup, esbuild)
- Faster transaction building and signing
Comparison with ethers.js
| Feature | viem | ethers.js |
|---|---|---|
| Bundle size | ~35kB | ~300kB |
| TypeScript support | Native, excellent | Good |
| API style | Functional | Object-oriented |
| Tree-shaking | Excellent | Good |
| Learning curve | Moderate | Easy |
| Documentation | Modern | Comprehensive |
| Community | Growing | Large, established |
Use viem if:
- Bundle size matters (mobile, low-bandwidth users)
- You want modern TypeScript features
- You prefer functional programming style
Use ethers.js if:
- You need maximum compatibility
- You're already using ethers.js
- Bundle size isn't critical
Next Steps
API Reference
createViemAdapter()
function createViemAdapter(
privateKey: `0x${string}`,
rpcUrl: string,
chain: Chain
): Promise<WalletAdapter>
Parameters:
privateKey- Wallet private key (with 0x prefix, typed as hex)rpcUrl- Ethereum RPC endpoint URLchain- viem Chain object (from viem/chains or custom)
Returns: Promise resolving to WalletAdapter
Example:
import { mainnet } from 'viem/chains';
const adapter = await createViemAdapter(
'0xabc123...' as `0x${string}`,
'https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY',
mainnet
);