Skip to main content

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:

  1. Module Details - Each module is documented in detail within its respective feature section
  2. Design Patterns - Common patterns are demonstrated throughout the documentation
  3. Provider Setup - Configure your blockchain provider
  4. Prerequisites - Set up your development environment