Skip to main content
Skill — Copyable reference implementation. Use as-is or customize. See Skills Philosophy.

Nonce Manager

When sending multiple Ethereum transactions, each requires a unique sequential nonce. Concurrent submissions cause collisions. Failed transactions create gaps. The NonceManager solves this.

The Problem

// Without nonce management - BROKEN
const [tx1, tx2, tx3] = await Promise.all([
  wallet.sendTransaction({ to, value }),
  wallet.sendTransaction({ to, value }),
  wallet.sendTransaction({ to, value }),
]);
// All three may get same nonce! Two will fail.
Race condition: Each call fetches nonce from chain (e.g., 5), then all three try to use nonce 5. Nonce gap: If tx with nonce 5 fails, tx with nonce 6 is stuck forever.

Quick Start

import { createNonceManager, jsonRpc } from '@voltaire/examples/nonce-manager';

// Create manager
const manager = createNonceManager({ source: jsonRpc() });

// Concurrent transactions - WORKS
const [nonce1, nonce2, nonce3] = await Promise.all([
  manager.consume({ address, chainId: 1, provider }),
  manager.consume({ address, chainId: 1, provider }),
  manager.consume({ address, chainId: 1, provider }),
]);
// nonces are sequential: 5, 6, 7

How It Works

The key insight from ethers v6 and viem:
Increment delta BEFORE awaiting chain nonce
async consume(params) {
  const promise = this.get(params);  // Start fetch
  this.increment(params);            // Increment BEFORE await
  const nonce = await promise;
  return nonce;
}
This allows concurrent callers to get unique nonces without blocking.

State Management

deltaMap     = { "0xabc.1": 2 }    // Pending tx count
nonceMap     = { "0xabc.1": 5 }    // Last confirmed nonce
promiseMap   = { "0xabc.1": P }    // Cached chain fetch

Final nonce = chain_nonce + delta

API Reference

createNonceManager(options?)

Creates a new nonce manager instance.
const manager = createNonceManager({
  source: jsonRpc(),  // or inMemory() for testing
  cacheSize: 8192,    // LRU cache entries
});

manager.consume(params)

Get and increment nonce atomically. Primary method for sending transactions.
const nonce = await manager.consume({
  address: '0x...',
  chainId: 1,
  provider: client,
});

manager.get(params)

Get next nonce without incrementing. For dry-run or estimation.
const nonce = await manager.get({ address, chainId, provider });

manager.increment(params)

Increment delta manually. Use when you know a tx will be sent.
manager.increment({ address, chainId });

manager.reset(params)

Clear cached state. Forces refetch on next get.
manager.reset({ address, chainId });

manager.recycle(params)

Decrement delta after tx failure. Allows nonce reuse.
try {
  await sendTx(nonce);
} catch {
  manager.recycle({ address, chainId });
}

manager.getDelta(params)

Get current pending tx count. For debugging.
console.log(manager.getDelta({ address, chainId })); // 3

Concurrent Transactions Pattern

import { createNonceManager, jsonRpc } from '@voltaire/examples/nonce-manager';

const manager = createNonceManager({ source: jsonRpc() });

async function sendBatchedTransactions(
  signer: Signer,
  transactions: Transaction[]
) {
  const address = await signer.getAddress();
  const chainId = 1;
  const provider = signer.provider;

  // Get all nonces concurrently
  const nonces = await Promise.all(
    transactions.map(() =>
      manager.consume({ address, chainId, provider })
    )
  );

  // Send all transactions
  const txPromises = transactions.map((tx, i) =>
    signer.sendTransaction({ ...tx, nonce: nonces[i] })
  );

  return Promise.all(txPromises);
}

Signer Wrapper Pattern

Like ethers NonceManager, wrap a signer for automatic nonce management:
import { wrapSigner } from '@voltaire/examples/nonce-manager';

// Wrap your signer
const managed = wrapSigner(wallet, { chainId: 1 });

// Nonces managed automatically
await managed.sendTransaction({ to, value });
await managed.sendTransaction({ to, value });

// Reset if needed
await managed.resetNonce();

Error Recovery

Transaction Failure

const nonce = await manager.consume({ address, chainId, provider });

try {
  await wallet.sendTransaction({ ...tx, nonce });
} catch (error) {
  // Transaction wasn't broadcast - recycle nonce
  manager.recycle({ address, chainId });
  throw error;
}

Stuck Transaction

If a transaction is stuck in mempool, your options:
  1. Wait - It may eventually confirm
  2. Speed up - Send replacement with same nonce, higher gas
  3. Cancel - Send 0-value tx to self with same nonce
// Speed up stuck tx
const stuckNonce = 5;
await wallet.sendTransaction({
  to: originalTx.to,
  value: originalTx.value,
  nonce: stuckNonce,
  maxFeePerGas: originalGas * 1.2n, // 20% higher
});

Nonce Gap Recovery

If you have a gap (e.g., nonces 5, 7 but not 6):
// Reset manager
manager.reset({ address, chainId });

// Fetch fresh nonce
const currentNonce = await manager.get({ address, chainId, provider });
// currentNonce will be 5 (the gap)

// Fill the gap
await wallet.sendTransaction({ to: address, value: 0n, nonce: currentNonce });

Multi-Chain Support

Nonces are scoped by address + chainId:
// Same address, different chains - independent nonces
await manager.consume({ address, chainId: 1, provider: mainnet });
await manager.consume({ address, chainId: 137, provider: polygon });

Testing with In-Memory Source

import { createNonceManager, inMemory } from '@voltaire/examples/nonce-manager';

const source = inMemory();
source.setNonce('0x123...', 1, 10); // Set initial nonce

const manager = createNonceManager({ source });

const nonce = await manager.consume({
  address: '0x123...',
  chainId: 1,
  provider: {}, // Provider not used with inMemory
});
// nonce === 10

Comparison with Libraries

ethers v6 NonceManager

// ethers
const managed = new NonceManager(wallet);
await managed.sendTransaction(tx);
  • Wraps signer
  • increment() / reset() methods
  • Tightly coupled to signer interface

viem nonceManager

// viem
const account = privateKeyToAccount(key, { nonceManager });
await sendTransaction(client, { account, ...tx });
  • Attached to account
  • consume() / get() / reset() methods
  • Decoupled, pluggable sources

Voltaire (this implementation)

// Standalone
const nonce = await manager.consume({ address, chainId, provider });

// Or wrapped
const managed = wrapSigner(wallet, { chainId });
await managed.sendTransaction(tx);
  • Both patterns supported
  • recycle() for failure recovery
  • getDelta() for debugging
  • Works with any provider interface