Architecture Overview
TokenOps SDK is designed with modularity, type safety, and extensibility as core principles. This guide explains the overall architecture and design decisions that make the SDK powerful and developer-friendly.
High-Level Architecture
Core Principles
1. Factory Pattern
All contract deployments use the factory pattern for consistent, upgradeable deployments:
// Factory creates contracts with consistent patterns
const factory = new TokenVestingManagerFactory(factoryAddress, provider);
const result = await factory.newTokenVestingManager(/* params */);
// Manager handles contract interactions
const manager = new TokenVestingManager(result.contractAddress, tokenAddress, provider);
2. Provider Abstraction
The SDK abstracts blockchain interactions through a provider system:
interface IProviderAdapter {
// Core blockchain operations
call(params: CallParams): Promise<any>;
sendTransaction(params: TransactionParams): Promise<string>;
getBalance(address: string): Promise<bigint>;
// ... more methods
}
3. Type Safety
Full TypeScript support with comprehensive type definitions:
// All parameters are typed
interface VestingParams {
recipient: string;
startTimestamp: bigint;
endTimestamp: bigint;
cliffAmount: bigint;
linearVestAmount: bigint;
isRevocable: boolean;
}
4. Event-Driven Architecture
All contracts emit typed events that are automatically parsed:
interface VestingCreatedEvent {
vestingId: bigint;
recipient: string;
amount: bigint;
startTime: bigint;
endTime: bigint;
}
Module Structure
/src
Directory Layout
src/
├── API/ # REST API client and types
│ ├── apiWrapper.ts # Main API client
│ └── types.ts # API response types
├── airdrop-v2/ # Airdrop system
│ ├── distributors/ # Merkle distributors
│ ├── factories/ # Deployment factories
│ └── utils/ # Merkle tree utilities
├── disperse-v2/ # Token distribution
│ ├── disperseGasFee.ts # Gas-based disperse
│ └── disperseTokenFee.ts # Token-based disperse
├── provider/ # Blockchain provider abstraction
│ ├── viemAdapter.ts # Viem implementation
│ ├── ethersAdapter.ts # Ethers implementation
│ └── base.ts # Abstract base class
├── staking-contracts-v1/ # Staking mechanisms
│ ├── staking.ts # Standard staking
│ ├── stakingWithUnbonding.ts # Unbonding staking
│ └── factories/ # Staking factories
├── utils/ # Shared utilities
│ ├── constants.ts # Contract addresses
│ ├── types.ts # Common types
│ └── helpers.ts # Helper functions
├── vesting-contracts-v3/ # Vesting contracts
│ ├── tokenVestingManager.ts # Standard vesting
│ ├── nativeTokenVestingManager.ts # Native token vesting
│ ├── tokenVestingManagerWithVotes.ts # Voting vesting
│ ├── milestoneVestingManager.ts # Milestone vesting
│ └── factories/ # Vesting factories
└── index.ts # Main exports
Design Patterns
1. Factory + Manager Pattern
Each contract type follows a consistent pattern:
// 1. Factory for deployment
class ContractFactory {
constructor(factoryAddress: string, provider: IProviderAdapter) {}
async newContract(...params): Promise<DeploymentResult> {}
}
// 2. Manager for interactions
class ContractManager {
constructor(contractAddress: string, provider: IProviderAdapter) {}
async performAction(...params): Promise<ActionResult> {}
async queryState(...params): Promise<StateResult> {}
}
2. Provider Strategy Pattern
Different blockchain libraries are supported through adapters:
abstract class BaseProviderAdapter implements IProviderAdapter {
abstract call(params: CallParams): Promise<any>;
abstract sendTransaction(params: TransactionParams): Promise<string>;
// ... other abstract methods
}
class ViemProviderAdapter extends BaseProviderAdapter {
// Viem-specific implementation
}
class EthersProviderAdapter extends BaseProviderAdapter {
// Ethers-specific implementation
}
3. Configuration Object Pattern
Complex operations use configuration objects:
interface VestingConfig {
recipient: string;
schedule: VestingSchedule;
cliff?: CliffConfig;
revocable: boolean;
funding: FundingConfig;
}
await manager.createVesting(config);
Data Flow
1. Contract Deployment Flow
2. Contract Interaction Flow
Error Handling
1. Error Hierarchy
abstract class TokenOpsError extends Error {
abstract code: string;
abstract userMessage: string;
}
class TransactionError extends TokenOpsError {
code = 'TRANSACTION_FAILED';
constructor(public txHash: string, public reason: string) {
super(`Transaction failed: ${reason}`);
}
}
class ContractError extends TokenOpsError {
code = 'CONTRACT_ERROR';
constructor(public contractAddress: string, public method: string) {
super(`Contract call failed: ${method} on ${contractAddress}`);
}
}
2. Error Recovery
try {
await manager.createVesting(params);
} catch (error) {
if (error instanceof TransactionError) {
// Handle transaction failures
console.log(`Transaction ${error.txHash} failed: ${error.reason}`);
} else if (error instanceof ContractError) {
// Handle contract interaction errors
console.log(`Contract error on ${error.contractAddress}`);
}
}
Performance Considerations
1. Lazy Loading
Contracts and providers are initialized only when needed:
class TokenVestingManager {
private _contract?: Contract;
private get contract() {
if (!this._contract) {
this._contract = this.provider.getContract(this.address, ABI);
}
return this._contract;
}
}
2. Batch Operations
Multiple operations can be batched for efficiency:
const batch = [
manager.createVesting(params1),
manager.createVesting(params2),
manager.createVesting(params3)
];
const results = await Promise.all(batch);
3. Caching Strategy
Frequently accessed data is cached:
class APIClient {
private cache = new Map<string, { data: any, timestamp: number }>();
async getData(key: string): Promise<any> {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < 60000) {
return cached.data;
}
const data = await this.fetchFromAPI(key);
this.cache.set(key, { data, timestamp: Date.now() });
return data;
}
}
Extensibility
1. Custom Providers
You can implement custom provider adapters:
class CustomProviderAdapter extends BaseProviderAdapter {
async call(params: CallParams): Promise<any> {
// Your custom implementation
}
async sendTransaction(params: TransactionParams): Promise<string> {
// Your custom implementation
}
}
2. Plugin System
The SDK supports plugins for extended functionality:
interface Plugin {
name: string;
init(sdk: TokenOpsSDK): void;
beforeTransaction?(params: TransactionParams): Promise<TransactionParams>;
afterTransaction?(result: TransactionResult): Promise<void>;
}
class LoggingPlugin implements Plugin {
name = 'logging';
init(sdk: TokenOpsSDK) {
// Initialize plugin
}
async beforeTransaction(params: TransactionParams) {
console.log('Transaction starting:', params);
return params;
}
}
Security Model
1. Input Validation
All inputs are validated at the SDK level:
function validateAddress(address: string): void {
if (!isValidAddress(address)) {
throw new ValidationError('Invalid Ethereum address');
}
}
function validateAmount(amount: bigint): void {
if (amount <= 0n) {
throw new ValidationError('Amount must be positive');
}
}
2. Safe Defaults
The SDK uses safe defaults for all operations:
const defaultGasLimit = 500000n;
const defaultSlippage = 0.5; // 0.5%
const defaultTimeout = 120000; // 2 minutes
Testing Architecture
The SDK includes comprehensive testing at multiple levels:
1. Unit Tests
- Individual function testing
- Mocked dependencies
- Edge case coverage
2. Integration Tests
- Contract interaction testing
- Provider compatibility testing
- End-to-end workflows
3. Network Tests
- Testnet deployment testing
- Real transaction testing
- Performance benchmarking
Next Steps
Now that you understand the architecture:
- Module Details - Each module is documented in detail within its respective feature section
- Design Patterns - Common patterns are demonstrated throughout the documentation
- Provider Setup - Configure your blockchain provider
- Prerequisites - Set up your development environment