Demos
Send Transaction

Send Transaction

This example shows how to send a multicall transaction using the useContractWrite hook. We also show how to use the useWaitForTransaction hook to fetch the transaction receipt to display a success or error message.

Send Transaction

Change the number of transactions to send:

1

The useContractWrite hook

This hook accepts a list of Starknet calls object, represented as a list of Call objects. The hook returns, among other things, a write method that submits the transaction, and a writeAsync method that submits the transaction and returns a promise with the transaction object. You should use write if you're not interested in using the return value, and writeAsync if you are. The value returned by writeAsync is also available in data.

Building the Call list

Use the Contract.populateTransaction functions to easily build a list of Call objects. First we instantiate a Contract object using the useContract hook. In this example, we connect it to the network's native currency contract.

send-transaction.tsx
1
import {
2
useAccount,
3
useContract,
4
useContractWrite,
5
useNetwork,
6
} from "@starknet-react/core";
7

8
function MyComponent() {
9
const { address } = useAccount();
10
const { chain } = useNetwork();
11

12
const { contract } = useContract({
13
abi: erc20ABI,
14
address: chain.nativeCurrency.address,
15
});
16
}

The next step is to build a list of calls based on user's input. In this example, we create a number of calls equal to an user-provided counter. Notice how we use contract.populateTransaction to build a Call object using a syntax similar to a function call.

send-transaction.tsx
1
import {
2
useAccount,
3
useContract,
4
useContractWrite,
5
useNetwork,
6
} from "@starknet-react/core";
7

8
function MyComponent() {
9
const { address } = useAccount();
10
const { chain } = useNetwork();
11

12
const { contract } = useContract({
13
abi: erc20ABI,
14
address: chain.nativeCurrency.address,
15
});
16

17
const [count, setCount] = useState(1);
18

19
const calls = useMemo(() => {
20
if (!address || !contract) return [];
21

22
return Array.from({ length: count }, (_, i) => {
23
const amount = uint256.bnToUint256(BigInt(i));
24
return contract.populateTransaction["transfer"]!(address, amount);
25
});
26
}, [contract, address, count]);
27

28
const {
29
write,
30
reset,
31
data: tx,
32
isLoading: isSubmitting,
33
isError: isSubmitError,
34
error: submitError,
35
} = useContractWrite({
36
calls,
37
});
38
}

Fetching the transaction receipt

We fetch the transaction receipt using the useWaitForTransaction hook. Notice that before the user submits their transaction, the value of tx?.transaction_hash is undefined and the hook won't fetch any data. Only after the user submits their transaction this value will become available and the hook will start fetching data. We set the retry flag to true to refetch data if the request fails. This can happen if the RPC provider is slower at picking up pending data.

send-transaction.tsx
1
import {
2
useAccount,
3
useContract,
4
useContractWrite,
5
useNetwork,
6
} from "@starknet-react/core";
7

8
function MyComponent() {
9
const { address } = useAccount();
10
const { chain } = useNetwork();
11

12
const { contract } = useContract({
13
abi: erc20ABI,
14
address: chain.nativeCurrency.address,
15
});
16

17
const [count, setCount] = useState(1);
18

19
const calls = useMemo(() => {
20
if (!address || !contract) return [];
21

22
return Array.from({ length: count }, (_, i) => {
23
const amount = uint256.bnToUint256(BigInt(i));
24
return contract.populateTransaction["transfer"]!(address, amount);
25
});
26
}, [contract, address, count]);
27

28
const {
29
write,
30
reset,
31
data: tx,
32
isLoading: isSubmitting,
33
isError: isSubmitError,
34
error: submitError,
35
} = useContractWrite({
36
calls,
37
});
38

39
const {
40
data: receipt,
41
isLoading,
42
isError,
43
error,
44
} = useWaitForTransaction({
45
hash: tx?.transaction_hash,
46
watch: true,
47
retry: true,
48
refetchInterval: 2000,
49
});
50
}

Adding the UI

We can now connect the data from the hooks to our UI. In this example, we customize the button icon and text based on the current interaction and transaction status. We also ensure that the user can restart the flow by clicking the button after the transaction has been accepted.

send-transaction.tsx
1
import {
2
useAccount,
3
useContract,
4
useContractWrite,
5
useNetwork,
6
} from "@starknet-react/core";
7

8
function MyComponent() {
9
const { address } = useAccount();
10
const { chain } = useNetwork();
11

12
const { contract } = useContract({
13
abi: erc20ABI,
14
address: chain.nativeCurrency.address,
15
});
16

17
const [count, setCount] = useState(1);
18

19
const calls = useMemo(() => {
20
if (!address || !contract) return [];
21

22
return Array.from({ length: count }, (_, i) => {
23
const amount = uint256.bnToUint256(BigInt(i));
24
return contract.populateTransaction["transfer"]!(address, amount);
25
});
26
}, [contract, address, count]);
27

28
const {
29
write,
30
reset,
31
data: tx,
32
isLoading: isSubmitting,
33
isError: isSubmitError,
34
error: submitError,
35
} = useContractWrite({
36
calls,
37
});
38

39
const {
40
data: receipt,
41
isLoading,
42
isError,
43
error,
44
} = useWaitForTransaction({
45
hash: tx?.transaction_hash,
46
watch: true,
47
retry: true,
48
refetchInterval: 2000,
49
});
50

51
const buttonContent = useMemo(() => {
52
if (isSubmitting) {
53
return (
54
<>
55
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
56
Send Transactions
57
</>
58
);
59
}
60

61
if (isLoading) {
62
return (
63
<>
64
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
65
Waiting for confirmation
66
</>
67
);
68
}
69

70
if (receipt && receipt.status === "REJECTED") {
71
return (
72
<>
73
<Cross className="h-4 w-4 mr-2" />
74
Transaction rejected
75
</>
76
);
77
}
78

79
if (receipt) {
80
return (
81
<>
82
<Check className="h-4 w-4 mr-2" />
83
Transaction confirmed
84
</>
85
);
86
}
87

88
return (
89
<>
90
<SendHorizonal className="h-4 w-4 mr-2" />
91
Send Transactions
92
</>
93
);
94
}, [isSubmitting, isLoading, receipt]);
95

96
const action = () => receipt ? reset() : write({});
97

98
return (
99
<Card className="mx-auto max-w-[400px]">
100
<CardHeader>
101
<CardTitle>Send Transaction</CardTitle>
102
</CardHeader>
103
<CardContent className="space-y-4">
104
<p>Change the number of transactions to send:</p>
105
<div className="w-full flex flex-row justify-center items-center">
106
<Button
107
variant="outline"
108
size="icon"
109
onClick={() => setCount((c) => Math.max(0, c - 1))}
110
>
111
<Minus className="h-4 w-4" />
112
</Button>
113
<span className="mx-8 text-center">{count}</span>
114
<Button
115
variant="outline"
116
size="icon"
117
onClick={() => setCount((c) => c + 1)}
118
>
119
<Plus className="h-4 w-4" />
120
</Button>
121
</div>
122
<Button
123
className="w-full"
124
onClick={action}
125
disabled={!address || isSubmitting || isLoading}
126
>
127
{buttonContent}
128
</Button>
129
{isSubmitError ? (
130
<Alert variant="destructive">
131
<AlertCircle className="h-4 w-4" />
132
<AlertTitle>Error</AlertTitle>
133
<AlertDescription>{submitError?.message}</AlertDescription>
134
</Alert>
135
) : null}
136
{isError ? (
137
<Alert variant="destructive">
138
<AlertCircle className="h-4 w-4" />
139
<AlertTitle>Error</AlertTitle>
140
<AlertDescription>{error?.message}</AlertDescription>
141
</Alert>
142
) : null}
143
</CardContent>
144
</Card>
145
);
146
}