Skip to main content
Source: poll.ts • Tests: poll.test.ts
This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.

Polling

Poll operations until they succeed or timeout, with optional exponential backoff. Essential for waiting on asynchronous Ethereum operations like transaction confirmations, block finalization, or contract state changes.

Overview

Polling utilities provide:
  • Configurable intervals: Control polling frequency
  • Exponential backoff: Progressively longer intervals
  • Timeout handling: Fail after maximum duration
  • Validation: Custom success conditions
  • Progress callbacks: Monitor polling attempts

Basic Usage

Simple Polling

Poll until result is truthy:
import { poll } from '@tevm/voltaire/utils';

const receipt = await poll(
  () => provider.eth_getTransactionReceipt(txHash),
  {
    interval: 1000,
    timeout: 60000
  }
);

With Validation

Poll until validation passes:
const block = await poll(
  () => provider.eth_getBlockByNumber('latest'),
  {
    interval: 1000,
    validate: (block) => block.number >= targetBlock
  }
);

Poll Options

PollOptions

interface PollOptions<T> {
  interval?: number;        // Polling interval in ms (default: 1000)
  timeout?: number;         // Max duration in ms (default: 60000)
  backoff?: boolean;        // Use exponential backoff (default: false)
  backoffFactor?: number;   // Backoff factor (default: 1.5)
  maxInterval?: number;     // Max interval with backoff (default: 10000)
  validate?: (result: T) => boolean;
  onPoll?: (result: T, attempt: number) => void;
}

Polling Functions

poll

Core polling function with full configuration:
const data = await poll(
  () => provider.eth_call({ to, data }),
  {
    interval: 500,
    timeout: 30000,
    validate: (result) => result !== '0x',
    onPoll: (result, attempt) => {
      console.log(`Attempt ${attempt}: ${result}`);
    }
  }
);

pollUntil

More expressive when checking specific conditions:
import { pollUntil } from '@tevm/voltaire/utils';

const blockNumber = await pollUntil(
  () => provider.eth_blockNumber(),
  (num) => num >= 1000000n,
  { interval: 500, timeout: 30000 }
);

pollForReceipt

Convenience function for transaction receipts:
import { pollForReceipt } from '@tevm/voltaire/utils';

const receipt = await pollForReceipt(
  txHash,
  (hash) => provider.eth_getTransactionReceipt(hash),
  { timeout: 120000 } // 2 minutes
);

if (receipt.status === '0x1') {
  console.log('Transaction successful!');
}

pollWithBackoff

Poll with exponential backoff enabled by default:
import { pollWithBackoff } from '@tevm/voltaire/utils';

const data = await pollWithBackoff(
  () => provider.eth_call({ to, data }),
  {
    interval: 100,      // Start at 100ms
    backoffFactor: 2,   // Double each time
    maxInterval: 5000,  // Cap at 5s
    timeout: 30000
  }
);

Exponential Backoff

How It Works

With backoff enabled, intervals increase exponentially:
interval = min(interval * backoffFactor, maxInterval)

Example Timeline

Configuration: { interval: 1000, backoffFactor: 1.5, maxInterval: 10000 }
AttemptInterval (ms)
1st1000
2nd1500
3rd2250
4th3375
5th5062
6th7593
7th+10000 (capped)

When to Use Backoff

Use backoff when:
  • Operation may take progressively longer
  • Want to reduce server load over time
  • Waiting for finality (takes longer as confirmations increase)
Don’t use backoff when:
  • Need consistent check frequency
  • Time-sensitive operations
  • Known fixed interval required

Real-World Examples

Wait for Transaction

Wait for transaction confirmation with backoff:
async function waitForTransaction(
  txHash: string,
  confirmations: number = 1
): Promise<TransactionReceipt> {
  const receipt = await pollForReceipt(
    txHash,
    (hash) => provider.eth_getTransactionReceipt(hash),
    {
      interval: 1000,
      timeout: 120000,
      backoff: true,
      backoffFactor: 1.5
    }
  );

  if (confirmations > 1) {
    const targetBlock = receipt.blockNumber + BigInt(confirmations - 1);

    await pollUntil(
      () => provider.eth_blockNumber(),
      (num) => num >= targetBlock,
      { interval: 12000 } // Block time
    );
  }

  return receipt;
}

Wait for Contract Deployment

Poll until contract code is deployed:
const code = await pollUntil(
  () => provider.eth_getCode(contractAddress),
  (code) => code.length > 2, // More than '0x'
  {
    interval: 1000,
    timeout: 60000,
    backoff: true
  }
);

console.log('Contract deployed!');

Wait for State Change

Poll contract state until condition met:
import { Abi } from '@tevm/voltaire/Abi';

const abi = [
  {
    type: 'function',
    name: 'balanceOf',
    inputs: [{ type: 'address', name: 'account' }],
    outputs: [{ type: 'uint256', name: 'balance' }]
  }
] as const;

const targetBalance = 1000000n;

await poll(
  async () => {
    const data = Abi.Function.encodeParams(abi[0], [address]);
    const result = await provider.eth_call({
      to: tokenAddress,
      data
    });
    return Abi.Function.decodeResult(abi[0], result)[0];
  },
  {
    interval: 2000,
    validate: (balance: bigint) => balance >= targetBalance,
    onPoll: (balance, attempt) => {
      console.log(
        `Attempt ${attempt}: ${balance}/${targetBalance}`
      );
    }
  }
);

Wait for Block Number

Wait for specific block with progress:
async function waitForBlock(targetBlock: bigint): Promise<void> {
  await pollUntil(
    () => provider.eth_blockNumber(),
    (current) => current >= targetBlock,
    {
      interval: 12000, // Average block time
      timeout: 300000, // 5 minutes
      onPoll: (current, attempt) => {
        const remaining = targetBlock - current;
        console.log(
          `Block ${current}/${targetBlock} (${remaining} remaining)`
        );
      }
    }
  );
}

Multiple Conditions

Poll until multiple conditions satisfied:
await poll(
  async () => {
    const [receipt, blockNumber] = await Promise.all([
      provider.eth_getTransactionReceipt(txHash),
      provider.eth_blockNumber()
    ]);
    return { receipt, blockNumber };
  },
  {
    interval: 1000,
    validate: ({ receipt, blockNumber }) => {
      if (!receipt) return false;
      if (receipt.status !== '0x1') return false;

      const confirmations = blockNumber - receipt.blockNumber;
      return confirmations >= 3;
    }
  }
);

Combining with Other Utils

Poll with Retry

Retry each poll attempt:
import { poll, retryWithBackoff } from '@tevm/voltaire/utils';

const data = await poll(
  () => retryWithBackoff(
    () => provider.eth_call({ to, data }),
    { maxRetries: 3 }
  ),
  {
    interval: 1000,
    validate: (result) => result !== '0x'
  }
);

Poll with Timeout Per Attempt

Add timeout to each poll attempt:
import { poll, withTimeout } from '@tevm/voltaire/utils';

const result = await poll(
  () => withTimeout(
    provider.eth_getBlockByNumber('latest'),
    { ms: 5000 }
  ),
  {
    interval: 1000,
    timeout: 60000
  }
);

Poll with Rate Limiting

Rate limit polling attempts:
import { poll, RateLimiter } from '@tevm/voltaire/utils';

const limiter = new RateLimiter({
  maxRequests: 10,
  interval: 1000
});

const receipt = await poll(
  () => limiter.execute(
    () => provider.eth_getTransactionReceipt(txHash)
  ),
  {
    interval: 500,
    timeout: 60000
  }
);

Error Handling

Handle Polling Errors

Errors during polling don’t stop the poll:
try {
  const receipt = await poll(
    () => provider.eth_getTransactionReceipt(txHash),
    { timeout: 60000 }
  );
} catch (error) {
  if (error.message.includes('timeout')) {
    console.log('Transaction not confirmed within 60s');
  }
}

Validation Errors

Distinguish between errors and unmet conditions:
const block = await poll(
  async () => {
    const block = await provider.eth_getBlockByNumber('latest');
    if (!block) {
      throw new Error('Failed to fetch block');
    }
    return block;
  },
  {
    validate: (block) => block.number >= targetBlock,
    interval: 1000
  }
);

Best Practices

Choose Appropriate Intervals

  • Transaction receipts: 1000ms (typical block time ~12s)
  • Block numbers: 12000ms (matches block time)
  • Contract state: 2000-5000ms (depends on update frequency)
  • Off-chain data: 5000-10000ms (depends on API)

Set Reasonable Timeouts

  • Fast operations: 30000ms (30s)
  • Transaction confirmation: 120000ms (2 minutes)
  • Multi-block operations: 300000ms (5 minutes)

Use Backoff for Long Operations

Enable backoff when:
  • Operation may take minutes
  • Checking less frequently over time is acceptable
  • Want to reduce server load

Monitor Progress

Always use onPoll callback for observability:
const receipt = await pollForReceipt(
  txHash,
  (hash) => provider.eth_getTransactionReceipt(hash),
  {
    timeout: 120000,
    onPoll: (receipt, attempt) => {
      logger.info('Polling for receipt', {
        txHash,
        attempt,
        found: receipt !== null
      });
    }
  }
);

API Reference

poll

async function poll<T>(
  fn: () => Promise<T>,
  options?: PollOptions<T>
): Promise<T>

pollUntil

async function pollUntil<T>(
  fn: () => Promise<T>,
  predicate: (result: T) => boolean,
  options?: Omit<PollOptions<T>, 'validate'>
): Promise<T>

pollForReceipt

async function pollForReceipt<TReceipt>(
  txHash: string,
  getReceipt: (hash: string) => Promise<TReceipt | null>,
  options?: Omit<PollOptions<TReceipt | null>, 'validate'>
): Promise<TReceipt>

pollWithBackoff

async function pollWithBackoff<T>(
  fn: () => Promise<T>,
  options?: PollOptions<T>
): Promise<T>

See Also