Cashu-TS - v4.1.0
    Preparing search index...

    DocumentsDeterministic Counters

    Deterministic counters (persist, inspect, bump)

    Deterministic outputs use per-keyset counters. The wallet reserves them atomically and emits a single event you can use to persist the "next" value in your storage.

    API at a glance:

    • wallet.counters.peekNext(id) – returns the current "next" for a keyset
    • wallet.counters.advanceToAtLeast(id, n) – bump forward if behind
    • wallet.on.countersReserved(cb) – subscribe to reservations (see WalletEvents for subscription patterns)

    ** Optional:** - Depends on CounterSource:

    These methods will throw if the CounterSource does not support them.

    • wallet.counters.snapshot() – inspect current overall state
    • wallet.counters.setNext(id, n) – hard-set for migrations/tests
    // 1) Seed once at app start if you have previously saved "next" per keyset
    const wallet = new Wallet(mintUrl, {
    unit: 'sat',
    bip39seed,
    keysetId: preferredKeysetId, // e.g. '0111111'
    counterInit: loadCountersFromDb(), // e.g. { '0111111': 128 }
    });
    await wallet.loadMint();

    // Alternative to using counterInit for individual keyset allocation
    await wallet.counters.advanceToAtLeast('0111111', 128);

    // 2) Subscribe once, persist future reservations
    wallet.on.countersReserved(({ keysetId, start, count, next }) => {
    // next is start + count (i.e: next available)
    saveNextToDb(keysetId, next); // do an atomic upsert per keysetId
    });

    // 3) Inspect current state, what will be reserved next
    const nextCounter = await wallet.counters.peekNext('0111111'); // 128

    // 4) After a restore or cross device sync, bump the cursor forward
    const { lastCounterWithSignature } = await wallet.batchRestore();
    if (lastCounterWithSignature != null) {
    const next = lastCounterWithSignature + 1; // e.g. 137
    await wallet.counters.advanceToAtLeast('0111111', next);
    await saveNextToDb('0111111', next);
    }

    // 5) Parallel keysets without mutation
    const wA = wallet; // bound to '0111111'
    const wB = wallet.withKeyset('0122222'); // bound to '0122222', same CounterSource
    await wB.counters.advanceToAtLeast('0122222', 10);
    await wA.counters.snapshot(); // { '0111111': 137, '0122222': 10 }
    await wB.counters.snapshot(); // { '0111111': 137, '0122222': 10 }
    wA.keysetId; // '0111111'
    wB.keysetId; // '0122222'

    // 6) Switch wallet default keyset and bump counter
    await wallet.counters.snapshot(); // { '0111111': 137, '0122222': 10 }
    wallet.keysetId; // '0111111'
    wallet.bindKeyset('0133333'); // bound to '0133333', same CounterSource
    wallet.keysetId; // '0133333'
    await wallet.counters.advanceToAtLeast('0133333', 456);

    // Counters persist per keyset, so rebinding does not reset the old one
    await wallet.counters.snapshot(); // { '0111111': 137, '0122222': 10, '0133333': 456 }
    await wA.counters.snapshot(); // { '0111111': 137, '0122222': 10, '0133333': 456 }
    await wB.counters.snapshot(); // { '0111111': 137, '0122222': 10, '0133333': 456 }

    Note The wallet does not await your callback. If saveNextToDb (or similar) is async, handle errors to avoid unhandled rejections For more on lifecycle management, see WalletEvents


    By default each new Wallet(...) creates its own internal counter source. If your app creates multiple wallet instances for the same seed (e.g. short-lived wallets per operation), each instance gets an independent copy seeded from counterInit — and concurrent operations can reserve overlapping counter ranges, causing "outputs have already been signed" errors.

    Use createEphemeralCounterSource() to create a single shared source and pass it to every wallet via the counterSource option:

    import { Wallet, createEphemeralCounterSource } from '@cashu/cashu-ts';

    // Create once at app start, seeded from your persisted counters
    const counters = createEphemeralCounterSource(loadCountersFromDb());

    // Every wallet instance shares the same source — no overlapping reservations
    const walletA = new Wallet(mintA, { unit: 'sat', bip39seed, counterSource: counters });
    const walletB = new Wallet(mintB, { unit: 'sat', bip39seed, counterSource: counters });

    The ephemeral source is memory-only — counters do not survive page reloads. Use wallet.on.countersReserved to persist after every operation:

    function wireCounterPersistence(wallet: Wallet) {
    wallet.on.countersReserved(({ keysetId, next }) => {
    saveNextToDb(keysetId, next);
    });
    }

    wireCounterPersistence(walletA);
    wireCounterPersistence(walletB);

    Because the source is shared, the global event on any wallet instance reflects the true cursor — there is no need for per-operation onCountersReserved callbacks in your builder chains.

    Option When to use
    counterInit Single wallet instance, or you don't need cross-wallet coordination. Seeds a wallet-local ephemeral source.
    counterSource Multiple wallet instances for the same seed, or you need persistence/custom storage. Takes precedence over counterInit.

    createEphemeralCounterSource returns the built-in in-memory implementation. For durable storage you can implement CounterSource directly:

    import type { CounterSource, CounterRange } from '@cashu/cashu-ts';

    class IndexedDbCounterSource implements CounterSource {
    async reserve(keysetId: string, n: number): Promise<CounterRange> {
    // atomic read-and-increment in your DB
    }
    async advanceToAtLeast(keysetId: string, minNext: number): Promise<void> {
    // conditional update: SET next = max(next, minNext)
    }
    // Optional: snapshot(), setNext()
    }