Skip to main content

Token Vesting Manager

The TokenVestingManager is the core vesting contract for ERC20 tokens in the TokenOps SDK. It provides flexible, secure token distribution with customizable schedules, cliff periods, and revocation mechanisms.

Overview

The TokenVestingManager contract manages the gradual release of ERC20 tokens according to predefined schedules. It's perfect for employee compensation, investor distributions, advisor payments, and any scenario requiring controlled token release.

Key Features

  • Flexible Schedules: Linear, cliff, step, or hybrid vesting patterns
  • Revocable/Non-Revocable: Configurable revocation for different use cases
  • Partial/Full Funding: Deploy with full funding or allow partial funding
  • Batch Operations: Create multiple vesting schedules efficiently
  • Event Tracking: Comprehensive event system for monitoring
  • Admin Controls: Withdraw fees, manage transfers, and batch operations

Factory Deployment

All TokenVestingManager contracts are deployed through a factory pattern:

import { TokenVestingManagerFactory } from 'tokenops-sdk';

const factory = new TokenVestingManagerFactory(
factoryAddress, // Factory contract address
provider // ViemProviderAdapter
);

// Deploy new vesting manager
const deployment = await factory.newTokenVestingManager(
tokenAddress, // ERC20 token address
fundingType, // BigInt: 0 = full funding, 1 = partial funding
{
account: deployerAddress, // Deployer address
value: BigInt(0) // ETH value for gas fees
}
);

console.log('Vesting Manager deployed:', deployment.tokenVestingManager);

Funding Types

Full Funding (0): Contract must be fully funded before creating vesting schedules

  • More secure - ensures all tokens are available
  • Requires upfront token commitment
  • Prevents over-allocation

Partial Funding (1): Vesting schedules can be created before full funding

  • Better cash flow management
  • Allows iterative funding
  • Requires careful management to prevent under-funding

Contract Interaction

Once deployed, interact with the contract using the TokenVestingManager class:

import { TokenVestingManager } from 'tokenops-sdk';

const manager = new TokenVestingManager(
managerAddress, // Vesting manager contract address
tokenAddress, // ERC20 token address
provider // ViemProviderAdapter
);

Creating Vesting Schedules

Basic Linear Vesting

Create a simple linear vesting schedule over time:

async function createLinearVesting() {
const now = Math.floor(Date.now() / 1000);

const vestingResult = await manager.createVesting(
recipientAddress, // Address receiving tokens
BigInt(now + 60), // Start: 1 minute from now
BigInt(now + (365 * 24 * 60 * 60)), // End: 1 year from now
BigInt(0), // Timelock: none
BigInt(0), // Initial unlock: none
BigInt(0), // Cliff time: none
BigInt(0), // Cliff amount: none
BigInt(30 * 24 * 60 * 60), // Release interval: monthly
BigInt('100000000000000000000000'), // Linear amount: 100,000 tokens
true, // Is revocable
{
account: creatorAddress, // Funding account
amount: BigInt('100000000000000000000000') // Gas fee amount
}
);

console.log('Linear vesting created:', vestingResult.vestingId);
return vestingResult.vestingId;
}

Employee Vesting (4-year with 1-year cliff)

Industry standard employee vesting schedule:

async function createEmployeeVesting(employeeAddress: Address) {
const now = Math.floor(Date.now() / 1000);
const oneYear = 365 * 24 * 60 * 60;
const fourYears = 4 * oneYear;

// 100,000 tokens total
// 25,000 at 1-year cliff
// 75,000 linear over remaining 3 years

const vestingResult = await manager.createVesting(
employeeAddress, // Recipient
BigInt(now), // Start: now
BigInt(now + fourYears), // End: 4 years
BigInt(0), // Timelock: none
BigInt(0), // Initial unlock: none
BigInt(now + oneYear), // Cliff: 1 year
BigInt('25000000000000000000000'), // Cliff amount: 25k tokens
BigInt(30 * 24 * 60 * 60), // Release interval: monthly
BigInt('75000000000000000000000'), // Linear amount: 75k tokens
true, // Revocable (employee can be terminated)
{
account: creatorAddress,
amount: BigInt(500000) // Gas fee
}
);

return vestingResult.vestingId;
}

Investor Vesting (2-year with 6-month cliff)

Typical investor vesting pattern:

async function createInvestorVesting(investorAddress: Address) {
const now = Math.floor(Date.now() / 1000);
const sixMonths = 6 * 30 * 24 * 60 * 60;
const twoYears = 2 * 365 * 24 * 60 * 60;

// 2,000,000 tokens total
// 500,000 at 6-month cliff
// 1,500,000 linear over remaining 18 months

const vestingResult = await manager.createVesting(
investorAddress,
BigInt(now),
BigInt(now + twoYears),
BigInt(0),
BigInt(0),
BigInt(now + sixMonths),
BigInt('500000000000000000000000'), // 500k at cliff
BigInt(7 * 24 * 60 * 60), // Weekly releases
BigInt('1500000000000000000000000'), // 1.5M linear
false, // Non-revocable
{
account: creatorAddress,
amount: BigInt(500000) // Gas fee
}
);

return vestingResult.vestingId;
}

Advisor Vesting (18-month with immediate unlock)

Advisor compensation with immediate partial unlock:

async function createAdvisorVesting(advisorAddress: Address) {
const now = Math.floor(Date.now() / 1000);
const eighteenMonths = 18 * 30 * 24 * 60 * 60;

// 50,000 tokens total
// 5,000 immediate unlock
// 45,000 linear over 18 months

const vestingResult = await manager.createVesting(
advisorAddress,
BigInt(now),
BigInt(now + eighteenMonths),
BigInt(0),
BigInt('5000000000000000000000'), // 5k immediate unlock
BigInt(0), // No cliff
BigInt(0),
BigInt(30 * 24 * 60 * 60), // Monthly releases
BigInt('45000000000000000000000'), // 45k linear
true, // Revocable
{
account: creatorAddress,
amount: BigInt(500000) // Gas fee
}
);

return vestingResult.vestingId;
}

Managing Vesting Schedules

Checking Vesting Status

// Get complete vesting information
async function getVestingInfo(vestingId: string) {
const vestingInfo = await manager.getVestingInfo(vestingId);

return {
recipient: vestingInfo.recipient,
startTimestamp: vestingInfo.startTimestamp,
endTimestamp: vestingInfo.endTimestamp,
deactivationTimestamp: vestingInfo.deactivationTimestamp,
timelock: vestingInfo.timelock,
releaseIntervalSecs: vestingInfo.releaseIntervalSecs,
cliffReleaseTimestamp: vestingInfo.cliffReleaseTimestamp,
initialUnlock: vestingInfo.initialUnlock,
cliffAmount: vestingInfo.cliffAmount,
linearVestAmount: vestingInfo.linearVestAmount,
claimedAmount: vestingInfo.claimedAmount,
isRevocable: vestingInfo.isRevocable
};
}

// Get basic vesting data (alternative method)
async function getVestingById(vestingId: string) {
return await manager.vestingById(vestingId);
}

// Check claimable amount
async function getClaimableAmount(vestingId: string) {
return await manager.getClaimableAmount(vestingId);
}

// Get vested amount at specific time
async function getVestedAmount(vestingId: string, timestamp: bigint) {
return await manager.getVestedAmount(vestingId, timestamp);
}

Claiming Tokens

Recipients can claim their vested tokens:

// Claim all available tokens
async function claimTokens(vestingId: string, recipientAddress: Address) {
const claimResult = await manager.claim(vestingId, {
account: recipientAddress,
amount: BigInt(500000) // Gas fee
});

console.log('Tokens claimed:', claimResult.withdrawalAmount);
return claimResult;
}

Revoking Vesting

For revocable vesting schedules, authorized users can revoke:

async function revokeVesting(vestingId: string, adminAddress: Address) {
// Only works for revocable vesting schedules
const revokeResult = await manager.revokeVesting(vestingId, {
account: adminAddress
});

console.log('Vesting revoked, withheld amount:', revokeResult.numTokensWithheld);
return revokeResult;
}

Batch Operations

Creating Multiple Vesting Schedules

async function createBatchVesting(vestingParams: BatchVestingParams) {
const batchResult = await manager.createVestingBatch(
vestingParams.recipients, // Array of recipient addresses
vestingParams.startTimestamps, // Array of start times
vestingParams.endTimestamps, // Array of end times
vestingParams.timelocks, // Array of timelock periods
vestingParams.initialUnlocks, // Array of initial unlock amounts
vestingParams.cliffTimestamps, // Array of cliff times
vestingParams.cliffAmounts, // Array of cliff amounts
vestingParams.releaseIntervals, // Array of release intervals
vestingParams.linearAmounts, // Array of linear amounts
vestingParams.revocableFlags, // Array of revocable flags
{
account: creatorAddress,
amount: BigInt(1000000) // Gas fee for batch
}
);

return batchResult;
}

interface BatchVestingParams {
recipients: Address[];
startTimestamps: bigint[];
endTimestamps: bigint[];
timelocks: bigint[];
initialUnlocks: bigint[];
cliffTimestamps: bigint[];
cliffAmounts: bigint[];
releaseIntervals: bigint[];
linearAmounts: bigint[];
revocableFlags: boolean[];
}

Batch Admin Operations

// Batch claim for multiple vestings (admin only)
async function batchAdminClaim(vestingIds: string[], adminAddress: Address) {
const batchClaimResult = await manager.batchAdminClaim(vestingIds, {
account: adminAddress,
amount: BigInt(1000000) // Gas fee
});

return batchClaimResult;
}

// Batch revoke multiple vestings (admin only)
async function batchRevokeVestings(vestingIds: string[], adminAddress: Address) {
const batchRevokeResult = await manager.batchRevokeVestings(vestingIds, {
account: adminAddress
});

return batchRevokeResult;
}

Vesting Transfers

Direct Transfer

async function transferVesting(vestingId: string, newOwner: Address, currentOwner: Address) {
const transferResult = await manager.directVestingTransfer(
vestingId,
newOwner,
{ account: currentOwner }
);

console.log('Vesting transferred:', transferResult);
return transferResult;
}

Cancel Transfer

async function cancelVestingTransfer(vestingId: string, currentOwner: Address) {
await manager.cancelVestingTransfer(vestingId, {
account: currentOwner
});

console.log('Vesting transfer cancelled');
}

Funding Management

Funding Vesting Schedules

// Fund a specific vesting (for partial funding contracts)
async function fundVesting(vestingId: string, amount: bigint, funderAddress: Address) {
const fundResult = await manager.fundVesting(vestingId, amount, {
account: funderAddress,
amount: BigInt(500000) // Gas fee
});

return fundResult;
}

// Check funding status
async function checkFundingStatus(vestingId: string) {
const fundingInfo = await manager.getVestingFundingInfo(vestingId);
const isFullyFunded = await manager.isVestingFullyFunded(vestingId);

return {
fundingType: fundingInfo.fundingType,
totalFunded: fundingInfo.totalFunded,
totalRequired: fundingInfo.totalRequired,
isFullyFunded
};
}

Read Methods

Contract Information

// Get token address
const tokenAddress = await manager.tokenAddress();

// Get deployment block number
const deploymentBlock = await manager.deploymentBlockNumber();

// Get fee information
const feeType = await manager.feeType();
const fee = await manager.fee();
const feeCollector = await manager.feeCollector();
const basisPoints = await manager.BASIS_POINTS();

// Get funding type
const fundingType = await manager.fundingType();

// Get reserved amounts
const reservedForVesting = await manager.numTokensReservedForVesting();
const reservedForFee = await manager.numTokensReservedForFee();

Recipient Information

// Get all recipients
const allRecipients = await manager.getAllRecipients();

// Check if address is a recipient
const isRecipient = await manager.isRecipient(userAddress);

// Get recipients length
const recipientCount = await manager.getAllRecipientsLength();

// Get recipients slice
const recipientSlice = await manager.getAllRecipientsSliced(
BigInt(0), // From index
BigInt(10) // To index
);

// Get specific recipient by index
const recipient = await manager.recipients(BigInt(0));

Vesting-Specific Information

// Get vesting funding amount
const vestingFunding = await manager.vestingFunding(vestingId);

// Get pending transfer info
const pendingTransfer = await manager.pendingVestingTransfers(vestingId);

Event Monitoring

Monitor vesting contract events for real-time tracking:

// Listen for vesting creation events
manager.on('VestingCreated', (event) => {
console.log('New vesting created:', {
vestingId: event.vestingId,
recipient: event.recipient,
vesting: event.vesting
});
});

// Listen for claim events
manager.on('Claimed', (event) => {
console.log('Tokens claimed:', {
vestingId: event.vestingId,
recipient: event.recipient,
amount: event.withdrawalAmount
});

// Send notification to recipient
sendClaimNotification(event.recipient, event.withdrawalAmount);
});

// Listen for funding events
manager.on('VestingFunded', (event) => {
console.log('Vesting funded:', {
vestingId: event.vestingId,
funder: event.funder,
amount: event.amount,
totalFunded: event.totalFunded,
totalRequired: event.totalRequired
});
});

// Listen for transfer events
manager.on('VestingTransferred', (event) => {
console.log('Vesting transferred:', {
previousOwner: event.previousOwner,
newOwner: event.newOwner,
vestingId: event.vestingId
});
});

// Listen for revocation events
manager.on('VestingRevoked', (event) => {
console.log('Vesting revoked:', {
vestingId: event.vestingId,
tokensWithheld: event.numTokensWithheld,
vesting: event.vesting
});
});

Administrative Functions

Fee Withdrawals

// Withdraw token fees
async function withdrawTokenFee(recipient: Address, amount: bigint, adminAddress: Address) {
const withdrawResult = await manager.withdrawTokenFee(recipient, amount, {
account: adminAddress
});

return withdrawResult;
}

// Withdraw gas fees
async function withdrawGasFee(recipient: Address, amount: bigint, adminAddress: Address) {
const withdrawResult = await manager.withdrawGasFee(recipient, amount, {
account: adminAddress
});

return withdrawResult;
}

// Withdraw other ERC20 tokens
async function withdrawOtherToken(tokenAddress: Address, recipient: Address, amount: bigint, adminAddress: Address) {
const withdrawResult = await manager.withdrawOtherToken(tokenAddress, recipient, amount, {
account: adminAddress
});

return withdrawResult;
}

Admin Operations

// Admin claim for specific vesting
async function adminClaim(vestingId: string, adminAddress: Address) {
const claimResult = await manager.adminClaim(vestingId, {
account: adminAddress,
amount: BigInt(500000) // Gas fee
});

return claimResult;
}

// Withdraw admin funds
async function withdrawAdmin(amount: bigint, adminAddress: Address) {
const withdrawResult = await manager.withdrawAdmin(amount, {
account: adminAddress
});

return withdrawResult;
}

// Get amount available for admin withdrawal
async function getAmountAvailableToWithdrawByAdmin() {
return await manager.amountAvailableToWithdrawByAdmin();
}

Advanced Use Cases

Dynamic Vesting Monitoring

class VestingMonitor {
private manager: TokenVestingManager;
private vestingIds: string[] = [];

constructor(manager: TokenVestingManager) {
this.manager = manager;
this.setupEventListeners();
}

private setupEventListeners() {
this.manager.on('VestingCreated', (event) => {
this.vestingIds.push(event.vestingId);
this.checkVestingStatus(event.vestingId);
});

this.manager.on('Claimed', (event) => {
this.logClaimEvent(event);
});
}

private async checkVestingStatus(vestingId: string) {
const vestingInfo = await this.manager.getVestingInfo(vestingId);
const claimable = await this.manager.getClaimableAmount(vestingId);

if (claimable > 0) {
console.log(`Vesting ${vestingId} has ${claimable} tokens claimable`);
}
}

private logClaimEvent(event: any) {
console.log(`${event.recipient} claimed ${event.withdrawalAmount} tokens`);
}

async getVestingsSummary() {
const summary = {
totalVestings: this.vestingIds.length,
totalClaimable: BigInt(0),
totalClaimed: BigInt(0)
};

for (const vestingId of this.vestingIds) {
const vestingInfo = await this.manager.getVestingInfo(vestingId);
const claimable = await this.manager.getClaimableAmount(vestingId);

summary.totalClaimable += claimable;
summary.totalClaimed += vestingInfo.claimedAmount;
}

return summary;
}
}

Vesting Calculator

class VestingCalculator {
static calculateVestingProgress(
startTimestamp: bigint,
endTimestamp: bigint,
cliffTimestamp: bigint,
totalAmount: bigint,
claimedAmount: bigint,
currentTimestamp: bigint
): VestingProgress {
const now = currentTimestamp;
const start = startTimestamp;
const end = endTimestamp;
const cliff = cliffTimestamp;

let vestedAmount = BigInt(0);
let claimableAmount = BigInt(0);

if (now >= start) {
if (cliff > 0 && now < cliff) {
// Before cliff - no vesting
vestedAmount = BigInt(0);
} else if (now >= end) {
// After end - fully vested
vestedAmount = totalAmount;
} else {
// During vesting period
const elapsed = now - start;
const duration = end - start;
vestedAmount = (totalAmount * elapsed) / duration;
}
}

claimableAmount = vestedAmount - claimedAmount;
if (claimableAmount < 0) claimableAmount = BigInt(0);

const progressPercentage = totalAmount > 0
? Number((vestedAmount * BigInt(100)) / totalAmount)
: 0;

return {
vestedAmount,
claimableAmount,
claimedAmount,
totalAmount,
progressPercentage,
isStarted: now >= start,
isCliffPassed: cliff === BigInt(0) || now >= cliff,
isCompleted: now >= end
};
}
}

interface VestingProgress {
vestedAmount: bigint;
claimableAmount: bigint;
claimedAmount: bigint;
totalAmount: bigint;
progressPercentage: number;
isStarted: boolean;
isCliffPassed: boolean;
isCompleted: boolean;
}

Security Best Practices

Input Validation

function validateVestingParams(params: VestingParams): void {
// Time validation
if (params.endTimestamp <= params.startTimestamp) {
throw new Error('End time must be after start time');
}

if (params.cliffTimestamp > 0 &&
(params.cliffTimestamp <= params.startTimestamp ||
params.cliffTimestamp >= params.endTimestamp)) {
throw new Error('Cliff time must be between start and end time');
}

// Amount validation
const totalAmount = params.cliffAmount + params.linearVestAmount + params.initialUnlock;
if (totalAmount <= 0) {
throw new Error('Total amount must be positive');
}

// Address validation
if (!params.recipient || params.recipient === '0x0000000000000000000000000000000000000000') {
throw new Error('Invalid recipient address');
}

// Interval validation
if (params.releaseInterval <= 0) {
throw new Error('Release interval must be positive');
}
}

interface VestingParams {
recipient: Address;
startTimestamp: bigint;
endTimestamp: bigint;
timelock: bigint;
initialUnlock: bigint;
cliffTimestamp: bigint;
cliffAmount: bigint;
releaseInterval: bigint;
linearVestAmount: bigint;
isRevocable: boolean;
}

Access Control

class SecureVestingManager {
private manager: TokenVestingManager;
private allowedCreators: Set<string>;
private adminAddress: Address;

constructor(
manager: TokenVestingManager,
allowedCreators: Address[],
adminAddress: Address
) {
this.manager = manager;
this.allowedCreators = new Set(allowedCreators.map(addr => addr.toLowerCase()));
this.adminAddress = adminAddress;
}

async createVesting(creator: Address, params: VestingParams): Promise<string> {
// Check authorization
if (!this.allowedCreators.has(creator.toLowerCase())) {
throw new Error('Unauthorized vesting creator');
}

// Validate parameters
validateVestingParams(params);

// Create vesting
const result = await this.manager.createVesting(
params.recipient,
params.startTimestamp,
params.endTimestamp,
params.timelock,
params.initialUnlock,
params.cliffTimestamp,
params.cliffAmount,
params.releaseInterval,
params.linearVestAmount,
params.isRevocable,
{ account: creator, amount: BigInt(500000) }
);

return result.vestingId;
}

async revokeVesting(vestingId: string, requester: Address): Promise<void> {
// Only admin can revoke
if (requester.toLowerCase() !== this.adminAddress.toLowerCase()) {
throw new Error('Only admin can revoke vesting');
}

await this.manager.revokeVesting(vestingId, { account: requester });
}
}

Troubleshooting

Common Issues

Issue: "Insufficient funding"

// Solution: Check if contract has enough tokens
const tokenBalance = await manager.numTokensReservedForVesting();
const vestingInfo = await manager.getVestingInfo(vestingId);
const requiredAmount = vestingInfo.cliffAmount + vestingInfo.linearVestAmount;

if (tokenBalance < requiredAmount) {
console.log('Contract needs more funding');
// For partial funding contracts, use fundVesting
await manager.fundVesting(vestingId, requiredAmount, {
account: funderAddress,
amount: BigInt(500000)
});
}

Issue: "Vesting not yet started"

// Solution: Check current time vs start time
const currentTime = Math.floor(Date.now() / 1000);
const vestingInfo = await manager.getVestingInfo(vestingId);

if (currentTime < vestingInfo.startTimestamp) {
console.log('Vesting has not started yet');
console.log('Starts at:', new Date(Number(vestingInfo.startTimestamp) * 1000));
}

Issue: "Nothing to claim"

// Solution: Check vesting progress
const claimable = await manager.getClaimableAmount(vestingId);
const vestingInfo = await manager.getVestingInfo(vestingId);

console.log('Claimable amount:', claimable);
console.log('Already claimed:', vestingInfo.claimedAmount);
console.log('Total cliff amount:', vestingInfo.cliffAmount);
console.log('Total linear amount:', vestingInfo.linearVestAmount);

// Check if cliff has passed
const currentTime = Math.floor(Date.now() / 1000);
if (vestingInfo.cliffReleaseTimestamp > 0 && currentTime < vestingInfo.cliffReleaseTimestamp) {
console.log('Cliff period has not ended yet');
}

Testing

Comprehensive testing for vesting functionality:

describe('TokenVestingManager', () => {
let manager: TokenVestingManager;
let vestingId: string;

beforeEach(async () => {
// Setup test environment
manager = new TokenVestingManager(contractAddress, tokenAddress, provider);
});

it('should create linear vesting correctly', async () => {
const now = Math.floor(Date.now() / 1000);
const vestingResult = await manager.createVesting(
recipientAddress,
BigInt(now),
BigInt(now + 365 * 24 * 60 * 60),
BigInt(0),
BigInt(0),
BigInt(0),
BigInt(0),
BigInt(30 * 24 * 60 * 60),
BigInt('100000000000000000000000'),
true,
{ account: creatorAddress, amount: BigInt(500000) }
);

expect(vestingResult.vestingId).toBeDefined();

const vestingInfo = await manager.getVestingInfo(vestingResult.vestingId);
expect(vestingInfo.linearVestAmount).toBe(BigInt('100000000000000000000000'));
});

it('should calculate claimable amount correctly', async () => {
// Create vesting and fast-forward time in test
const vestingId = await createLinearVesting();

// Mock time advancement (implementation depends on test framework)
await advanceTime(30 * 24 * 60 * 60); // 30 days

const claimable = await manager.getClaimableAmount(vestingId);

// Should be approximately 1/12 of total amount (30 days of 365 days)
expect(claimable).toBeGreaterThan(BigInt(0));
});

it('should handle cliff vesting correctly', async () => {
const vestingId = await createEmployeeVesting(employeeAddress);

// Before cliff - should be 0
const claimableBeforeCliff = await manager.getClaimableAmount(vestingId);
expect(claimableBeforeCliff).toBe(BigInt(0));

// After cliff - should have cliff amount
await advanceTime(365 * 24 * 60 * 60 + 1); // 1 year + 1 second

const claimableAfterCliff = await manager.getClaimableAmount(vestingId);
expect(claimableAfterCliff).toBeGreaterThan(BigInt('25000000000000000000000'));
});

it('should claim tokens successfully', async () => {
const vestingId = await createLinearVesting();
await advanceTime(30 * 24 * 60 * 60); // 30 days

const claimResult = await manager.claim(vestingId, {
account: recipientAddress,
amount: BigInt(500000)
});

expect(claimResult.withdrawalAmount).toBeGreaterThan(BigInt(0));
});
});

Performance Optimization

Efficient Claiming

class OptimizedClaimManager {
private manager: TokenVestingManager;
private claimQueue: Map<string, ClaimRequest> = new Map();

constructor(manager: TokenVestingManager) {
this.manager = manager;
}

async batchClaim(vestingIds: string[], recipient: Address): Promise<void> {
const claimableIds: string[] = [];

// Filter only vestings with claimable amounts
for (const vestingId of vestingIds) {
const claimable = await this.manager.getClaimableAmount(vestingId);
if (claimable > 0) {
claimableIds.push(vestingId);
}
}

// Process claims in batches to avoid gas limit issues
const batchSize = 10;
for (let i = 0; i < claimableIds.length; i += batchSize) {
const batch = claimableIds.slice(i, i + batchSize);

for (const vestingId of batch) {
try {
await this.manager.claim(vestingId, {
account: recipient,
amount: BigInt(500000)
});
} catch (error) {
console.error(`Failed to claim vesting ${vestingId}:`, error);
}
}

// Small delay between batches
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}

interface ClaimRequest {
vestingId: string;
recipient: Address;
timestamp: number;
}