Demos
Transaction Manager

Transaction Manager

This example shows how to create a transaction manager component.

Transaction Manager

Install dependencies

A transaction manager requires to persist state across several components. In React, this is done by using a state management library such as Jotai, Recoil (based on atoms) or Redux.

For this demo, we are going to use Jotai.

npm
pnpm
yarn

_10
npm install jotai

Create the transaction manager hook

Next we can create a transaction manager hook. We want to persist the transaction list across page reloads so we use Jotai's atomWithStorage function to create the atom.

hooks/useTransactionManager.ts

_15
import { useAtom } from "jotai";
_15
import { atomWithStorage } from "jotai/utils";
_15
_15
const transactionsAtom = atomWithStorage<string[]>(
_15
'userTransactions', []
_15
);
_15
_15
export function useTransactionManager() {
_15
const [value, setValue] = useAtom(transactionsAtom);
_15
_15
return {
_15
hashes: value,
_15
add: (hash: string) => setValue((prev) => [...prev, hash]),
_15
}
_15
}

Setup the contract write function

Now it's time to prepare the useContractWrite hook. This step depends on your dapp, for this demo we are going to send 1 wei to the connected wallet.

components/my-component.tsx

_26
function MyComponent() {
_26
const amount = uint256.bnToUint256(1n);
_26
const { address } = useAccount();
_26
const { chain } = useNetwork();
_26
_26
const { contract } = useContract({
_26
abi: erc20ABI,
_26
address: chain.nativeCurrency.address,
_26
});
_26
_26
const { writeAsync, isLoading } = useContractWrite({
_26
calls: address ? [
_26
contract?.populateTransaction["transfer"]!(address, amount),
_26
] : [],
_26
});
_26
_26
return (
_26
<Card className="max-w-[400px] mx-auto">
_26
<CardHeader>
_26
<CardTitle>Transaction Manager</CardTitle>
_26
</CardHeader>
_26
<CardContent className="space-y-4">
_26
</CardContent>
_26
</Card>
_26
);
_26
}

Tracking a transaction

Instead of calling the writeAsync function directly, we are going to create a wrapper function that sends the transaction and then adds it to our transaction manager.

components/my-component.tsx

_46
function MyComponent() {
_46
const amount = uint256.bnToUint256(1n);
_46
const { address } = useAccount();
_46
const { chain } = useNetwork();
_46
_46
const { contract } = useContract({
_46
abi: erc20ABI,
_46
address: chain.nativeCurrency.address,
_46
});
_46
_46
const { writeAsync, isLoading } = useContractWrite({
_46
calls: address ? [
_46
contract?.populateTransaction["transfer"]!(address, amount),
_46
] : [],
_46
});
_46
_46
const { hashes, add } = useTransactionManager();
_46
_46
const submitTx = useCallback(async () => {
_46
const tx = await writeAsync({});
_46
add(tx.transaction_hash);
_46
}, [writeAsync]);
_46
_46
return (
_46
<Card className="max-w-[400px] mx-auto">
_46
<CardHeader>
_46
<CardTitle>Transaction Manager</CardTitle>
_46
</CardHeader>
_46
<CardContent className="space-y-4">
_46
<Button
_46
variant="default"
_46
onClick={submitTx}
_46
className="w-full"
_46
disabled={!address}
_46
>
_46
{isLoading ? (
_46
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
_46
) : (
_46
<SendHorizonal className="h-4 w-4 mr-2" />
_46
)}
_46
Submit Transaction
_46
</Button>
_46
</CardContent>
_46
</Card>
_46
);
_46
}

Visualizing the transactions

Finally, we create a component that fetches the transaction receipt and displays its status. This component uses the useWaitForTransaction hook to fetch the transaction receipt. Notice that we use the watch flag to refresh the receipt at every block, and the retry flag to retry fetching data on error. This is needed because the RPC provider may return a "not found" error for a few seconds after we submit our transaction.

components/transaction-status.tsx

_31
function TransactionStatus({ hash }: { hash: string }) {
_31
const {
_31
data,
_31
error,
_31
isLoading,
_31
isError
_31
} = useWaitForTransaction({
_31
hash,
_31
watch: true,
_31
retry: true,
_31
});
_31
_31
return (
_31
<div className="flex items-center w-full">
_31
<div className="space-y-1 w-full">
_31
<p className="text-sm font-medium leading-none overflow-hidden text-ellipsis">
_31
{hash}
_31
</p>
_31
<p className="text-sm font-muted-foreground">
_31
{isLoading
_31
? "Loading..."
_31
: isError
_31
? error?.message
_31
: data?.status === "REJECTED"
_31
? `${data?.status}`
_31
: `${data?.execution_status} - ${data?.finality_status}`}
_31
</p>
_31
</div>
_31
</div>
_31
);
_31
}

The last step is to use this component in our main component.

components/my-component.tsx

_55
function MyComponent() {
_55
const amount = uint256.bnToUint256(1n);
_55
const { address } = useAccount();
_55
const { chain } = useNetwork();
_55
_55
const { contract } = useContract({
_55
abi: erc20ABI,
_55
address: chain.nativeCurrency.address,
_55
});
_55
_55
const { writeAsync, isLoading } = useContractWrite({
_55
calls: address ? [
_55
contract?.populateTransaction["transfer"]!(address, amount),
_55
] : [],
_55
});
_55
_55
const { hashes, add } = useTransactionManager();
_55
_55
const submitTx = useCallback(async () => {
_55
const tx = await writeAsync({});
_55
add(tx.transaction_hash);
_55
}, [writeAsync]);
_55
_55
return (
_55
<Card className="max-w-[400px] mx-auto">
_55
<CardHeader>
_55
<CardTitle>Transaction Manager</CardTitle>
_55
</CardHeader>
_55
<CardContent className="space-y-4">
_55
<Button
_55
variant="default"
_55
onClick={submitTx}
_55
className="w-full"
_55
disabled={!address}
_55
>
_55
{isLoading ? (
_55
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
_55
) : (
_55
<SendHorizonal className="h-4 w-4 mr-2" />
_55
)}
_55
Submit Transaction
_55
</Button>
_55
<Separator />
_55
<div className="space-y-4">
_55
<div className="hidden last:block">
_55
Submitted transactions will appear here.
_55
</div>
_55
{hashes.map((hash) => (
_55
<TransactionStatus key={hash} hash={hash} />
_55
))}
_55
</div>
_55
</CardContent>
_55
</Card>
_55
);
_55
}