Track Transactions in React with Pulsar
This guide adds Pulsar to a React app, step by step, for an EVM or a Solana wallet. At the end you have:
- a transaction store from
@tuwaio/pulsar-core, saved tolocalStorage; - the chain adapter from
@tuwaio/pulsar-evmor@tuwaio/pulsar-solana, which tracks each transaction in the background; @tuwaio/pulsar-reactrestarting the trackers after a page reload;- a button that sends a contract (EVM) or program (Solana) call and tracks it until its final status.
The guide installs the Pulsar packages one by one. The Next.js + Pulsar Packages template on Starter Templates is a complete app built the same way.
Every step has an EVM and a Solana tab. The tabs are linked: pick your network once and every step follows it.
Before You Start
- React 19.2 or newer. The examples use the Next.js App Router:
'use client'modules and the@/import alias forsrc/. - A wallet connection. This guide uses Nova Connect on Satellite Connect: follow its README to render
SatelliteConnectProvider, the watcher of your network andNovaConnectProvider. Pulsar does not require Nova: the EVM adapter works with any@wagmi/coreconfig, and the Solana adapter reads the connection that Satellite Connect saves. - Your network configuration in one module, which the code below imports:
EVM
wagmiConfig (your @wagmi/core config) and appChains (its viem chains) exported from @/configs/wagmiConfig.
Pulsar validates the metadata of every transaction before anything else runs: each title string must be 100 characters or less, each description string 300 characters or less, and the JSON of payload 10 KB or less, without executable-like strings. Invalid metadata makes executeTxAction throw PulsarTransactionValidationError.
Step 1: Install the Packages
Install the Pulsar packages of your network and their peer dependencies:
EVM
pnpm add @tuwaio/pulsar-core @tuwaio/pulsar-evm @tuwaio/pulsar-react \
@tuwaio/orbit-core @tuwaio/orbit-evm @wagmi/core viem zustand immer dayjsStep 2: Describe the Contract or Program
EVM
You need the address and the ABI (Application Binary Interface) of the contract you call. This guide uses a Counter contract deployed on Sepolia:
// A Counter contract deployed on Sepolia.
export const COUNTER_ADDRESS = '0xAe7f46914De82028eCB7E2bF97Feb3D3dDCc2BAB';
export const CounterAbi = [
{
inputs: [],
name: 'decrement',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'getCurrentNumber',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'increment',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
] as const;Step 3: Create an Action
An action is a function that asks the wallet to sign and submit the transaction and returns its key. Pulsar calls it inside executeTxAction and tracks the transaction under the returned key. Keep actions in their own files to reuse them from several components.
EVM
import { type Config, writeContract } from '@wagmi/core';
import { sepolia } from 'viem/chains';
import { COUNTER_ADDRESS, CounterAbi } from '@/abis/CounterAbi';
// Returns the transaction hash, which Pulsar uses as the txKey.
export function increment({ wagmiConfig }: { wagmiConfig: Config }) {
return writeContract(wagmiConfig, {
abi: CounterAbi,
address: COUNTER_ADDRESS,
functionName: 'increment',
chainId: sepolia.id,
});
}Optional: the same call from an ERC-4337 smart account. With @tuwaio/orbit-evm you can send the call as a UserOperation from a Solady smart account owned by the connected wallet. The action returns the userOpHash. Pulsar does not detect UserOperations on its own: Step 6 passes tracker: TransactionTracker.ERC4337 and your pimlicoApiKey. Pulsar then tracks it in two stages: the bundler until the UserOperation is included, then the bundle transaction on-chain. The ERC-4337 guide explains the lifecycle.
import { createPimlicoSmartAccountClient } from '@tuwaio/orbit-evm';
import type { Config } from '@wagmi/core';
import { encodeFunctionData } from 'viem';
import { sepolia } from 'viem/chains';
import { COUNTER_ADDRESS, CounterAbi } from '@/abis/CounterAbi';
// Returns the userOpHash, which Pulsar uses as the txKey.
export async function incrementWithSmartAccount({
wagmiConfig,
pimlicoApiKey,
}: {
wagmiConfig: Config;
pimlicoApiKey?: string;
}) {
// A Solady smart account owned by the connected wallet, sent through the Pimlico bundler.
// With an API key, gas is sponsored by the Pimlico paymaster by default.
const { account, bundlerClient } = await createPimlicoSmartAccountClient({
chain: sepolia,
wagmiConfig,
apiKey: pimlicoApiKey,
});
return bundlerClient.sendUserOperation({
account,
calls: [
{
to: COUNTER_ADDRESS,
data: encodeFunctionData({ abi: CounterAbi, functionName: 'increment' }),
},
],
});
}Step 4: Create the Transaction Store
Create the store once, in a client module. createPulsarStore connects Pulsar to your network through its adapter and saves the transactions to localStorage under name. createBoundedUseStore turns the vanilla store into a React hook. beforeTxProcess is an optional check that runs before the wallet is asked to sign: throw to block the transaction.
EVM
'use client';
import { createBoundedUseStore, createPulsarStore, type EvmTransaction } from '@tuwaio/pulsar-core';
import { pulsarEvmAdapter } from '@tuwaio/pulsar-evm';
import { appChains, wagmiConfig } from '@/configs/wagmiConfig';
// Typed transactions of your app
type IncrementTx = EvmTransaction & {
type: 'increment';
payload: {
value: number;
};
};
export type TransactionUnion = IncrementTx;
export const pulsarStore = createPulsarStore<TransactionUnion>({
name: 'transactions-tracking-storage', // localStorage key
adapter: pulsarEvmAdapter(wagmiConfig, appChains),
beforeTxProcess: () => {
if (!navigator.onLine) throw new Error('You are offline.');
},
});
export const usePulsarStore = createBoundedUseStore(pulsarStore);An app with EVM and Solana wallets passes both adapters in an array: adapter: [pulsarEvmAdapter(wagmiConfig, appChains), pulsarSolanaAdapter({ rpcUrls: solanaRPCUrls })].
Step 5: Resume Tracking After a Reload
The store restores its transactions from localStorage, but their trackers are not running after a reload. useInitializeTransactionsPool from @tuwaio/pulsar-react starts them again for every pending transaction. Each call starts new trackers, so render this component only once, in a component that stays mounted (for example next to your providers in the root layout):
'use client';
import { useInitializeTransactionsPool } from '@tuwaio/pulsar-react';
import { usePulsarStore } from '@/hooks/txTrackingHooks';
export function PulsarInitializer() {
const initializeTransactionsPool = usePulsarStore((state) => state.initializeTransactionsPool);
// Restarts the trackers of transactions that were pending before a page reload.
useInitializeTransactionsPool({ initializeTransactionsPool });
return null;
}Step 6: Send and Track the Transaction
Call executeTxAction with the action and the metadata of the transaction. Pulsar validates the title, description and payload, checks the network of the wallet, runs beforeTxProcess, calls the action, adds the transaction to the pool and starts its tracker. From then on the store updates the status on its own: components read it with selectors, so it stays correct after navigation or a reload. A beforeTxProcess passed to executeTxAction replaces the one of the store for that transaction.
EVM
Pulsar asks the wallet to switch to desiredChainID when needed. For standard EVM transactions the promise of executeTxAction resolves only after tracking has finished, so render the status from the store rather than from the promise.
'use client';
import { ConnectButton } from '@tuwaio/nova-connect/components';
import { OrbitAdapter } from '@tuwaio/orbit-core';
import { TransactionTracker } from '@tuwaio/pulsar-core';
import { sepolia } from 'viem/chains';
import { wagmiConfig } from '@/configs/wagmiConfig';
import { usePulsarStore } from '@/hooks/txTrackingHooks';
import { increment } from '@/transactions/actions/increment';
import { incrementWithSmartAccount } from '@/transactions/actions/incrementWithSmartAccount';
const pimlicoApiKey = process.env.NEXT_PUBLIC_PIMLICO_API_KEY;
export const Increment = () => {
const executeTxAction = usePulsarStore((state) => state.executeTxAction);
const pendingCount = usePulsarStore(
(state) => Object.values(state.transactionsPool).filter((tx) => tx.pending).length,
);
const handleIncrement = async () => {
try {
await executeTxAction({
actionFunction: () => increment({ wagmiConfig }),
params: {
type: 'increment',
adapter: OrbitAdapter.EVM,
desiredChainID: sepolia.id, // the wallet is asked to switch to Sepolia if needed
title: ['Incrementing', 'Incremented', 'Increment failed', 'Increment replaced'],
description: 'Increment the counter by 1.',
payload: { value: 1 },
withTrackedModal: true, // opens the tracking modal of Nova Transactions
},
onSuccess: (tx) => console.log('Incremented in', tx.hash),
});
} catch (error) {
// Rejected signature, declined network switch, failed preflight... Also saved in `initialTx.error`.
console.error(error);
}
};
const handleIncrementWithSmartAccount = async () => {
try {
await executeTxAction({
actionFunction: () => incrementWithSmartAccount({ wagmiConfig, pimlicoApiKey }),
params: {
type: 'increment',
adapter: OrbitAdapter.EVM,
desiredChainID: sepolia.id,
title: 'Increment with a smart account',
payload: { value: 1 },
tracker: TransactionTracker.ERC4337, // required: UserOperations are not detected automatically
pimlicoApiKey, // saved with the transaction to resume tracking after a reload
},
});
} catch (error) {
console.error(error);
}
};
return (
<div className="flex flex-col items-start gap-4">
<ConnectButton />
<button type="button" onClick={handleIncrement}>
Increment
</button>
<button type="button" onClick={handleIncrementWithSmartAccount}>
Increment with a smart account
</button>
{pendingCount > 0 && <p>{pendingCount} pending transaction(s)</p>}
</div>
);
};Next Steps
- Show the transactions.
withTrackedModal: trueonly has an effect with Nova Transactions : renderNovaTransactionsProviderwith the state of your Pulsar store to get the tracking modal, toasts and the transaction history. - Track without the store, in your own state or on a server: EVM Trackers Standalone and Solana Trackers Standalone .
- Look up an API in the Pulsar package reference:
pulsar-core,pulsar-evm,pulsar-solana,pulsar-react. - Understand the failure modes that Pulsar handles: Why Web3 Transaction State is Broken.