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
1
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
1
import { useAtom } from "jotai";
2
import { atomWithStorage } from "jotai/utils";
3

4
const transactionsAtom = atomWithStorage<string[]>(
5
'userTransactions', []
6
);
7

8
export function useTransactionManager() {
9
const [value, setValue] = useAtom(transactionsAtom);
10

11
return {
12
hashes: value,
13
add: (hash: string) => setValue((prev) => [...prev, hash]),
14
}
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
1
function MyComponent() {
2
const amount = uint256.bnToUint256(1n);
3
const { address } = useAccount();
4
const { chain } = useNetwork();
5

6
const { contract } = useContract({
7
abi: erc20ABI,
8
address: chain.nativeCurrency.address,
9
});
10

11
const { writeAsync, isLoading } = useContractWrite({
12
calls: address ? [
13
contract?.populateTransaction["transfer"]!(address, amount),
14
] : [],
15
});
16

17
return (
18
<Card className="max-w-[400px] mx-auto">
19
<CardHeader>
20
<CardTitle>Transaction Manager</CardTitle>
21
</CardHeader>
22
<CardContent className="space-y-4">
23
</CardContent>
24
</Card>
25
);
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
1
function MyComponent() {
2
const amount = uint256.bnToUint256(1n);
3
const { address } = useAccount();
4
const { chain } = useNetwork();
5

6
const { contract } = useContract({
7
abi: erc20ABI,
8
address: chain.nativeCurrency.address,
9
});
10

11
const { writeAsync, isLoading } = useContractWrite({
12
calls: address ? [
13
contract?.populateTransaction["transfer"]!(address, amount),
14
] : [],
15
});
16

17
const { hashes, add } = useTransactionManager();
18

19
const submitTx = useCallback(async () => {
20
const tx = await writeAsync();
21
add(tx.transaction_hash);
22
}, [writeAsync]);
23

24
return (
25
<Card className="max-w-[400px] mx-auto">
26
<CardHeader>
27
<CardTitle>Transaction Manager</CardTitle>
28
</CardHeader>
29
<CardContent className="space-y-4">
30
<Button
31
variant="default"
32
onClick={submitTx}
33
className="w-full"
34
disabled={!address}
35
>
36
{isLoading ? (
37
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
38
) : (
39
<SendHorizonal className="h-4 w-4 mr-2" />
40
)}
41
Submit Transaction
42
</Button>
43
</CardContent>
44
</Card>
45
);
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
1
function TransactionStatus({ hash }: { hash: string }) {
2
const {
3
data,
4
error,
5
isLoading,
6
isError
7
} = useWaitForTransaction({
8
hash,
9
watch: true,
10
retry: true,
11
});
12

13
return (
14
<div className="flex items-center w-full">
15
<div className="space-y-1 w-full">
16
<p className="text-sm font-medium leading-none overflow-hidden text-ellipsis">
17
{hash}
18
</p>
19
<p className="text-sm font-muted-foreground">
20
{isLoading
21
? "Loading..."
22
: isError
23
? error?.message
24
: data?.status === "REJECTED"
25
? `${data?.status}`
26
: `${data?.execution_status} - ${data?.finality_status}`}
27
</p>
28
</div>
29
</div>
30
);
31
}

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

components/my-component.tsx
1
function MyComponent() {
2
const amount = uint256.bnToUint256(1n);
3
const { address } = useAccount();
4
const { chain } = useNetwork();
5

6
const { contract } = useContract({
7
abi: erc20ABI,
8
address: chain.nativeCurrency.address,
9
});
10

11
const { writeAsync, isLoading } = useContractWrite({
12
calls: address ? [
13
contract?.populateTransaction["transfer"]!(address, amount),
14
] : [],
15
});
16

17
const { hashes, add } = useTransactionManager();
18

19
const submitTx = useCallback(async () => {
20
const tx = await writeAsync();
21
add(tx.transaction_hash);
22
}, [writeAsync]);
23

24
return (
25
<Card className="max-w-[400px] mx-auto">
26
<CardHeader>
27
<CardTitle>Transaction Manager</CardTitle>
28
</CardHeader>
29
<CardContent className="space-y-4">
30
<Button
31
variant="default"
32
onClick={submitTx}
33
className="w-full"
34
disabled={!address}
35
>
36
{isLoading ? (
37
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
38
) : (
39
<SendHorizonal className="h-4 w-4 mr-2" />
40
)}
41
Submit Transaction
42
</Button>
43
<Separator />
44
<div className="space-y-4">
45
<div className="hidden last:block">
46
Submitted transactions will appear here.
47
</div>
48
{hashes.map((hash) => (
49
<TransactionStatus key={hash} hash={hash} />
50
))}
51
</div>
52
</CardContent>
53
</Card>
54
);
55
}