Skip to main content

Provider System Overview

The TokenOps SDK uses a provider abstraction layer to support multiple blockchain libraries while maintaining a consistent API. This design allows you to use your preferred web3 library (Viem, Ethers.js) without changing how you interact with the SDK.

Why Provider Abstraction?

1. Library Choice Freedom

Use the following blockchain libraries:

  • Viem: Modern, type-safe, lightweight
  • Ethers.js: Mature, widely adopted

2. Consistent API

All providers expose the same interface, making it easy to switch libraries:

// Same code works with any provider
const result = await manager.createVesting(params);

3. Future-Proof

New blockchain libraries can be supported by adding new provider adapters without breaking existing code.

Provider Architecture

Core Provider Interface

All providers implement the IProviderAdapter interface:

interface IProviderAdapter {
// Transaction operations
sendTransaction(params: TransactionParams): Promise<string>;
call(params: CallParams): Promise<any>;
estimateGas(params: GasEstimationParams): Promise<bigint>;

// Account operations
getBalance(address: string): Promise<bigint>;
getAccount(): Promise<string>;
signMessage(message: string): Promise<string>;

// Network operations
getChainId(): Promise<number>;
getBlockNumber(): Promise<bigint>;
getTransactionReceipt(hash: string): Promise<TransactionReceipt>;

// Contract operations
getContract(address: string, abi: any): any;
getLogs(filter: LogFilter): Promise<Log[]>;

// Utility operations
parseEvents(receipt: TransactionReceipt, abi: any): ParsedEvent[];
waitForTransaction(hash: string): Promise<TransactionReceipt>;
}

Available Providers

Modern, type-safe provider with excellent TypeScript support:

import { ViemProviderAdapter } from 'tokenops-sdk';
import { createPublicClient, createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const publicClient = createPublicClient({
chain: mainnet,
transport: http('YOUR_RPC_URL')
});

const walletClient = createWalletClient({
chain: mainnet,
transport: http('YOUR_RPC_URL'),
account: 'YOUR_ACCOUNT'
});

const provider = new ViemProviderAdapter(publicClient, walletClient);

Advantages:

  • Type-safe by design
  • Tree-shakeable and lightweight
  • Modern async/await patterns
  • Excellent error handling
  • Built-in multicall support

2. Ethers Provider

Mature provider with broad ecosystem support:

import { EthersProviderAdapter } from 'tokenops-sdk';
import { ethers } from 'ethers';

const ethersProvider = new ethers.JsonRpcProvider('YOUR_RPC_URL');
const wallet = new ethers.Wallet('YOUR_PRIVATE_KEY', ethersProvider);

const provider = new EthersProviderAdapter(wallet);

Advantages:

  • Mature and stable
  • Wide ecosystem support
  • Familiar API for many developers
  • Extensive documentation

Provider Comparison

FeatureViemEthers.js
Type Safety✅ Excellent🟡 Good
Bundle Size✅ Small🟡 Moderate
Performance✅ Fast🟡 Good
Learning Curve🟡 Moderate✅ Easy
Ecosystem🟡 Growing✅ Mature

Provider Selection Guide

Choose Viem if:

  • You're starting a new project
  • Type safety is important
  • Bundle size matters
  • You want modern JavaScript patterns

Choose Ethers.js if:

  • You have existing Ethers.js code
  • You need maximum ecosystem compatibility
  • Your team is familiar with Ethers.js
  • You're migrating from web3.js

Common Operations

Transaction Handling

// All providers support the same transaction interface
const txHash = await provider.sendTransaction({
to: contractAddress,
data: encodedFunctionCall,
value: BigInt(0),
gasLimit: BigInt(500000)
});

const receipt = await provider.waitForTransaction(txHash);
console.log('Transaction successful:', receipt.status === 'success');

Contract Interactions

// Contract calls work consistently across providers
const result = await provider.call({
to: contractAddress,
data: encodedFunctionCall
});

// Event parsing is standardized
const events = provider.parseEvents(receipt, contractABI);

Gas Estimation

// Gas estimation with automatic optimization
const gasEstimate = await provider.estimateGas({
to: contractAddress,
data: encodedFunctionCall,
from: userAddress
});

// Add 20% buffer for safety
const gasLimit = gasEstimate * BigInt(120) / BigInt(100);

Error Handling

Provider-Specific Errors

Each provider handles errors differently, but the SDK normalizes them:

try {
await provider.sendTransaction(params);
} catch (error) {
if (error instanceof TransactionError) {
console.log('Transaction failed:', error.reason);
} else if (error instanceof NetworkError) {
console.log('Network issue:', error.message);
}
}

Common Error Types

class ProviderError extends Error {
constructor(
public provider: string,
public originalError: any,
message: string
) {
super(message);
}
}

class TransactionError extends ProviderError {
constructor(
public txHash: string,
public reason: string,
originalError: any
) {
super('transaction', originalError, `Transaction failed: ${reason}`);
}
}

Testing with Providers

Mock Providers

For testing, you can create mock providers:

class MockProvider extends BaseProviderAdapter {
private responses = new Map();

setResponse(method: string, params: any, response: any) {
this.responses.set(`${method}-${JSON.stringify(params)}`, response);
}

async call(params: CallParams): Promise<any> {
const key = `call-${JSON.stringify(params)}`;
return this.responses.get(key) || '0x';
}

async sendTransaction(params: TransactionParams): Promise<string> {
return '0x1234567890abcdef...'; // Mock transaction hash
}
}

Test Utilities

// Test helper for provider operations
export function createTestProvider(): MockProvider {
const provider = new MockProvider();

// Set up common responses
provider.setResponse('getChainId', {}, 1);
provider.setResponse('getBlockNumber', {}, BigInt(12345));

return provider;
}

Migration Guide

From Ethers.js to Viem

// Before (Ethers.js)
const provider = new ethers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const ethersAdapter = new EthersProviderAdapter(wallet);

// After (Viem)
const publicClient = createPublicClient({
chain: mainnet,
transport: http(RPC_URL)
});

const walletClient = createWalletClient({
chain: mainnet,
transport: http(RPC_URL),
account: privateKeyToAccount(PRIVATE_KEY)
});

const viemAdapter = new ViemProviderAdapter(publicClient, walletClient);

Provider Switching

// Environment-based provider selection
function createProvider(): IProviderAdapter {
const providerType = process.env.PROVIDER_TYPE || 'viem';

switch (providerType) {
case 'viem':
return new ViemProviderAdapter(publicClient, walletClient);
case 'ethers':
return new EthersProviderAdapter(wallet);
default:
throw new Error(`Unknown provider type: ${providerType}`);
}
}

Best Practices

1. Provider Configuration

// Centralize provider configuration
export const providerConfig = {
rpcUrl: process.env.RPC_URL!,
chainId: parseInt(process.env.CHAIN_ID!),
privateKey: process.env.PRIVATE_KEY!,
gasMultiplier: 1.2
};

export function createConfiguredProvider(): ViemProviderAdapter {
// ... provider setup with config
}

2. Error Recovery

class ResilientProvider extends ViemProviderAdapter {
async sendTransaction(params: TransactionParams): Promise<string> {
let attempts = 0;
const maxAttempts = 3;

while (attempts < maxAttempts) {
try {
return await super.sendTransaction(params);
} catch (error) {
attempts++;
if (attempts === maxAttempts) throw error;

// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
}
}

throw new Error('Max retry attempts reached');
}
}

3. Monitoring

class MonitoredProvider extends ViemProviderAdapter {
async sendTransaction(params: TransactionParams): Promise<string> {
const startTime = Date.now();

try {
const result = await super.sendTransaction(params);
this.logMetrics('transaction_success', Date.now() - startTime);
return result;
} catch (error) {
this.logMetrics('transaction_error', Date.now() - startTime);
throw error;
}
}

private logMetrics(event: string, duration: number) {
console.log(`Provider metric: ${event} took ${duration}ms`);
}
}