Overview

@ethernauta/transaction

Transaction lifecycle tracking. Once a tx is broadcast, you need to know whether it’s pending, mined, reverted, or replaced. This package layers Trackable<T> and Watchable over the Readable<T> primitives in @ethernauta/eth.

pnpm add @ethernauta/transaction

Core idea

A Tracker is a Reader plus a Store. The store persists pending transaction metadata so a page reload doesn’t lose track of an outstanding broadcast. Polling for the receipt happens against the reader; the store is what makes the polling survive.

Setting up

import { create_tracker, window_store } from "@ethernauta/transaction";
import { encode_chain_id, http } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });

const tracker = create_tracker(
  [{ chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] }],
  { store: window_store },
);

create_store accepts any Storage-like backend — localStorage, sessionStorage, or your own implementation of the Store interface.

Registering a broadcast

import { create_tracker, register_transaction, window_store } from "@ethernauta/transaction";
import { encode_chain_id, http } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { Hash32Schema } from "@ethernauta/core";
import { parse } from "valibot";

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const tracker = create_tracker(
  [{ chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] }],
  { store: window_store },
);

const hash = parse(Hash32Schema, "0x" + "00".repeat(32));

// after broadcasting, register the hash
await register_transaction(hash)(tracker({ chain_id: CHAIN_ID }));

Stored as a PendingTransaction. Re-running the dapp later will pick it up on tracker construction.

set_transaction is the lower-level setter; register_transaction is the typical entry point.

Waiting for a receipt

import { create_tracker, wait_for_receipt, window_store } from "@ethernauta/transaction";
import { encode_chain_id, http } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { Hash32Schema } from "@ethernauta/core";
import { parse } from "valibot";

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const tracker = create_tracker(
  [{ chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] }],
  { store: window_store },
);
const hash = parse(Hash32Schema, "0x" + "00".repeat(32));

const receipt = await wait_for_receipt([hash])(
  tracker({ chain_id: CHAIN_ID }),
);
// → ConfirmedReceipt

Polls eth_getTransactionReceipt at the tracker’s interval, resolves when the receipt appears. Built on top of Readable<T> — no wallet needed.

Watching with a callback

import { create_tracker, watch_transaction, window_store } from "@ethernauta/transaction";
import { encode_chain_id, http } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { Hash32Schema } from "@ethernauta/core";
import { parse } from "valibot";

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const tracker = create_tracker(
  [{ chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] }],
  { store: window_store },
);
const hash = parse(Hash32Schema, "0x" + "00".repeat(32));

const unsubscribe = watch_transaction(hash, (transaction) => {
  console.log("status:", transaction.status);
})(tracker({ chain_id: CHAIN_ID }));

void unsubscribe;

Returns a Watchable — an unsubscribe function. Use when you want side effects on receipt arrival rather than awaiting.

Surface

ExportShapePurpose
create_tracker(CHAINS, { store })factoryBuild a tracker bound to a list of chains.
create_store({ namespace, backend })factoryPersistence layer.
register_transaction(args)Trackable<void>Add a broadcast to the registry.
set_transaction(args)Trackable<void>Lower-level setter.
wait_for_receipt(args)Trackable<ConfirmedReceipt>Resolve when mined.
watch_transaction(args)Trackable<Watchable>Subscribe to lifecycle events.
registryMap<Hash32, Transaction>In-memory mirror of the store.

Types

  • Tracker — the factory return type.
  • TrackContext, ResolvedTracker — the resolved shape passed to methods.
  • Trackable<T>(_resolved: ResolvedTracker) => Promise<T>.
  • Watchable() => void (unsubscribe).
  • Store{ get, set, delete, keys }.
  • PendingTransaction, ConfirmedReceipt, Transaction — record shapes.

Why a separate package

Tracking is read-only but stateful. Readable<T> is the shape of stateless reads; mixing in store-bound state would muddle the contract.

Splitting it out also lets dapps that don’t need persistence (one-shot scripts, server-side flows) avoid pulling in the storage abstraction.

See also