EVM Trackers Standalone
The trackers of @tuwaio/pulsar-evm also work without the Pulsar store. Use them when you:
- keep transactions in your own state (Redux, MobX, another Zustand store, a database);
- track transactions on a server, where there is no
localStorageand no wallet; - need a callback for each stage of the lifecycle instead of store updates.
Without the store nothing is persisted and nothing is resumed after a restart: you decide what to save and when to start tracking again. The trackers use only viem, @wagmi/core and HTTP APIs, so they run in the browser and in Node.js.
Why evmTracker Instead of waitForTransactionReceipt
| Situation | waitForTransactionReceipt (viem) | evmTracker (Pulsar) |
|---|---|---|
| The node has not indexed the tx | Waits for the receipt; the transaction details are not fetched. | Retries getTransaction (10 times, 3 s apart by default) and passes the details (nonce, fees, to) to a callback. |
| Transient RPC errors | The call rejects once its timeout or its own retries are exhausted. | Retries waiting for the receipt up to 5 times with a growing delay, then calls onFailure. |
| Speed-up or cancel in the wallet | Reported through onReplaced; the promise resolves with the new receipt. | Calls onReplaced and stops, so the replaced transaction is never reported as successful. |
| Several confirmations | confirmations option of the single call. | Polls confirmations every 5 s and reports each count through onConfirmationsUpdate. |
evmTracker calls onSuccess for every mined transaction, including reverted ones: check receipt.status.
Standard Transactions: evmTracker
evmTracker follows a transaction by its hash through the wagmi client of its chain, and resolves when tracking has finished:
import { evmTracker } from '@tuwaio/pulsar-evm';
import { createConfig, http } from '@wagmi/core';
import { sepolia } from 'viem/chains';
const wagmiConfig = createConfig({ chains: [sepolia], transports: { [sepolia.id]: http() } });
export async function trackTransaction(hash: `0x${string}`) {
await evmTracker({
config: wagmiConfig,
tx: { txKey: hash, chainId: sepolia.id, requiredConfirmations: 2 },
onTxDetailsFetched: (details) => console.log('Nonce', details.nonce),
onConfirmationsUpdate: (confirmations) => console.log(`${confirmations}/2 confirmations`),
onSuccess: async (_details, receipt) => {
console.log(receipt.status === 'success' ? 'Confirmed' : 'Reverted', receipt.blockNumber);
},
onReplaced: (replacement) => console.log(`Replaced (${replacement.reason}) by`, replacement.transaction.hash),
onFailure: (error) => console.error('Tracking failed', error),
});
}The chain must be configured in the wagmi config. Tracking fails at once for the zero hash or an unknown chain.
ERC-4337 UserOperations: erc4337Tracker
erc4337Tracker polls eth_getUserOperationReceipt every 2 s until the UserOperation is included in a bundle, and gives up after 60 consecutive failed requests. It calls onSuccess with the hash of the bundle transaction; pass that hash to evmTracker if you also need block confirmations:
import { erc4337Tracker, evmTracker } from '@tuwaio/pulsar-evm';
import { type Config } from '@wagmi/core';
import { sepolia } from 'viem/chains';
declare const wagmiConfig: Config;
export function trackUserOperation(userOpHash: `0x${string}`, pimlicoApiKey?: string) {
erc4337Tracker({
// Uses bundlerUrl if set, else api.pimlico.io with the API key, else the rate-limited public Pimlico endpoint.
tx: { txKey: userOpHash, chainId: sepolia.id, pimlicoApiKey, pending: true },
onSuccess: ({ hash }) => {
if (!hash) return;
void evmTracker({
config: wagmiConfig,
tx: { txKey: hash, chainId: sepolia.id },
onTxDetailsFetched: () => {},
onSuccess: async (_details, receipt) => console.log('Bundle transaction', receipt.status),
onReplaced: () => {},
onFailure: (error) => console.error(error),
});
},
onFailure: (result) => console.error('UserOperation failed:', result?.reason ?? 'too many failed requests'),
});
}erc4337Tracker returns immediately; polling runs in the background. A reverted UserOperation calls onFailure with its revert reason.
Safe Multisig and Gelato: Polling Fetchers
Safe and Gelato are tracked by polling an API. Pass their fetcher to initializePollingTracker from @tuwaio/pulsar-core, which calls it every 5 s by default and gives up after 10 consecutive failed requests (it then calls onFailure() without arguments).
[!NOTE] Gelato relay is deprecated. Use ERC-4337 UserOperations with
@tuwaio/orbit-evmanderc4337Trackerinstead.
import { initializePollingTracker } from '@tuwaio/pulsar-core';
import { createGelatoClient, gelatoFetcher, safeFetcher } from '@tuwaio/pulsar-evm';
// Safe: txKey is the safeTxHash, from is the Safe address.
export function trackSafeTransaction(safeTxHash: `0x${string}`, safeAddress: `0x${string}`, chainId: number) {
initializePollingTracker({
tx: { txKey: safeTxHash, chainId, from: safeAddress, pending: true },
fetcher: safeFetcher,
onSuccess: (status) => console.log('Executed in', status.transactionHash),
onFailure: (status) => console.error(status ? 'Execution failed' : 'Not found or too many failed requests'),
onReplaced: (executed) =>
console.warn('Another transaction with the same nonce was executed:', executed.safeTxHash),
});
}
// Gelato (deprecated): txKey is the task ID.
export function trackGelatoTask(taskId: string, apiKey: string) {
initializePollingTracker({
tx: { txKey: taskId, pending: true },
fetcher: gelatoFetcher(createGelatoClient({ apiKey })),
onSuccess: (status) => console.log('Task succeeded', status),
onFailure: (status) => console.error('Task failed', status),
});
}- Safe: the fetcher calls the Safe Transaction Service of the chain (see
SafeTransactionServiceUrlsfor the supported chains). A 404 response callsonFailure()without a response and stops. A transaction still pending one day after it was proposed callsonFailurewith its status and stops. - Gelato: a task still pending one hour after it was created calls
onFailurewith its status and stops.
These fetchers stop with withoutRemoving: true, so a removeTxFromPool you pass to initializePollingTracker is called only when polling gives up after maxRetries consecutive errors.
Helpers
checkTransactionsTracker({ actionTxKey, connectorType, tracker })returns the tracker Pulsar would use for a key:ERC4337orGelatoonly when requested,Safefor Safe connectors (evm:safe),Ethereumotherwise.speedUpTxActionandcancelTxActionresend a pending EIP-1559 transaction with the same nonce and fees raised by 15%. They need thenonceand fee fields the tracker fetched;evmTrackerthen reports the original transaction as replaced.selectEvmTxExplorerLinkbuilds the block explorer (or Safe app) link of a transaction.
import { TransactionTracker } from '@tuwaio/pulsar-core';
import { checkTransactionsTracker } from '@tuwaio/pulsar-evm';
const { tracker, txKey } = checkTransactionsTracker({ actionTxKey: '0xabc123', connectorType: 'evm:metamask' });
console.log(tracker === TransactionTracker.Ethereum, txKey); // true '0xabc123'The full list of exports is in the @tuwaio/pulsar-evm reference.