on 21d1c95 help sign in
bridge/src/lib/useEthereumTransaction.js
276 lines 7.1 kB history raw
1import { useCallback, useEffect, useState } from 'react';
2import { Just } from 'folktale/maybe';
3import { fromWei, toWei } from 'web3-utils';
5import { useNetwork } from 'store/network';
6import { useWallet } from 'store/wallet';
7import { usePointCursor } from 'store/pointCursor';
9import { GAS_LIMITS } from './constants';
10import {
11 signTransaction,
12 sendSignedTransaction,
13 waitForTransactionConfirm,
14 hexify,
15} from './txn';
16import * as need from './need';
17import { ensureFundsFor } from './tank';
18import useDeepEqualReference from './useDeepEqualReference';
20const STATE = {
21 NONE: 'NONE',
22 SIGNED: 'SIGNED',
23 BROADCASTED: 'BROADCASTED',
24 CONFIRMED: 'CONFIRMED',
25 COMPLETED: 'COMPLETED',
26};
28/**
29 * manage the state around sending and confirming an ethereum transaction
30 * @param {Transaction Function()} transactionBuilder
31 * @param {Promise<any> Function()} refetch async function called after completion
32 * @param {number} initialGasLimit gas limit
33 * @param {number} initialGasPrice gas price in gwei
34 */
35export default function useEthereumTransaction(
36 transactionBuilder,
37 refetch,
38 initialGasLimit = GAS_LIMITS.DEFAULT,
39 initialGasPrice = 20
40) {
41 const { wallet, walletType, walletHdPath } = useWallet();
42 const { web3, networkType } = useNetwork();
43 const { pointCursor } = usePointCursor();
45 const _web3 = need.web3(web3);
46 const _wallet = need.wallet(wallet);
48 const [error, _setError] = useState();
49 const setError = useCallback(
50 error => {
51 _setError(error);
52 if (error) {
53 console.error(error);
54 }
55 },
56 [_setError]
57 );
58 const [state, setState] = useState(STATE.NONE);
59 const [chainId, setChainId] = useState();
60 const [nonce, setNonce] = useState();
61 const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
62 const [suggestedGasPrice, setSuggestedGasPrice] = useState(gasPrice); // gwei
63 const [gasLimit] = useState(initialGasLimit);
64 const [unsignedTransaction, setUnsignedTransaction] = useState();
65 const [signedTransaction, setSignedTransaction] = useState();
66 const [txHash, setTxHash] = useState();
67 const [needFunds, setNeedFunds] = useState();
69 const initializing = nonce === undefined || chainId === undefined;
70 const constructed = !!unsignedTransaction;
71 const isDefaultState = state === STATE.NONE;
72 const signed = state === STATE.SIGNED;
73 const broadcasted = state === STATE.BROADCASTED;
74 const confirmed = state === STATE.CONFIRMED;
75 const completed = state === STATE.COMPLETED;
77 // lock inputs once we're out of the default state
78 const inputsLocked = !isDefaultState;
80 // we can sign when:
81 const canSign = !initializing && constructed && isDefaultState;
83 const construct = useCallback(
84 async (...args) =>
85 setUnsignedTransaction(await transactionBuilder(...args)),
86 [transactionBuilder]
87 );
89 const unconstruct = useCallback(() => setUnsignedTransaction(undefined), [
90 setUnsignedTransaction,
91 ]);
93 const generateAndSign = useCallback(async () => {
94 try {
95 setError(undefined);
97 const txn = await signTransaction({
98 wallet: _wallet,
99 walletType,
100 walletHdPath,
101 networkType,
102 txn: unsignedTransaction,
103 nonce,
104 chainId,
105 gasPrice: gasPrice.toFixed(0),
106 gasLimit,
107 });
109 setSignedTransaction(txn);
110 setState(STATE.SIGNED);
111 } catch (error) {
112 setError(error);
113 }
114 }, [
115 _wallet,
116 chainId,
117 gasLimit,
118 gasPrice,
119 networkType,
120 nonce,
121 setError,
122 unsignedTransaction,
123 walletHdPath,
124 walletType,
125 ]);
127 const broadcast = useCallback(async () => {
128 try {
129 setError(undefined);
131 const rawTx = hexify(signedTransaction.serialize());
132 const cost = toWei(gasPrice.toFixed(0), 'gwei') * gasLimit;
134 let usedTank = false;
135 // if this ethereum transaction is being executed by a specific point
136 // see if we can use the tank
137 if (Just.hasInstance(pointCursor)) {
138 usedTank = await ensureFundsFor(
139 _web3,
140 pointCursor.value,
141 _wallet.address,
142 cost,
143 [rawTx],
144 (address, minBalance, balance) =>
145 setNeedFunds({ address, minBalance, balance }),
146 () => setNeedFunds(undefined)
147 );
148 }
150 const txHash = await sendSignedTransaction(
151 _web3,
152 signedTransaction,
153 /* doubtNonceError */ usedTank
154 );
156 setState(STATE.BROADCASTED);
157 setTxHash(txHash);
159 await waitForTransactionConfirm(_web3, txHash);
161 setState(STATE.CONFIRMED);
162 } catch (error) {
163 setError(error);
164 }
165 }, [
166 _wallet.address,
167 _web3,
168 gasLimit,
169 gasPrice,
170 pointCursor,
171 setError,
172 signedTransaction,
173 ]);
175 const reset = useCallback(() => {
176 setTxHash(undefined);
177 setSignedTransaction(undefined);
178 setNonce(undefined);
179 setChainId(undefined);
180 setGasPrice(suggestedGasPrice);
181 setState(STATE.NONE);
182 setError(undefined);
183 setNeedFunds(undefined);
184 }, [suggestedGasPrice, setError]);
186 const resetGasPrice = useCallback(() => setGasPrice(suggestedGasPrice), [
187 setGasPrice,
188 suggestedGasPrice,
189 ]);
191 useEffect(() => {
192 // if nonce or chainId is undefined, re-fetch on-chain info
193 if (!(nonce === undefined || chainId === undefined)) {
194 return;
195 }
197 (async () => {
198 try {
199 setError(undefined);
200 const [nonce, chainId, estimatedGasPrice] = await Promise.all([
201 _web3.eth.getTransactionCount(_wallet.address),
202 _web3.eth.net.getId(),
203 _web3.eth.getGasPrice(),
204 ]);
206 setNonce(nonce);
207 setChainId(chainId);
208 const gasPrice = parseInt(fromWei(estimatedGasPrice, 'gwei'), 10);
209 setSuggestedGasPrice(gasPrice);
210 setGasPrice(gasPrice);
211 } catch (error) {
212 setError(error);
213 }
214 })();
215 }, [_wallet, _web3, setError, nonce, chainId]);
217 useEffect(() => {
218 let cancelled = false;
220 if (confirmed) {
221 (async () => {
222 if (refetch) {
223 try {
224 await refetch();
225 } catch (error) {
226 // log the original
227 console.error(error);
228 // track a user-friendly error
229 setError(
230 new Error(
231 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
232 )
233 );
234 }
235 }
237 if (cancelled) {
238 return;
239 }
241 setState(STATE.COMPLETED);
242 })();
243 }
245 return () => (cancelled = true);
246 }, [confirmed, refetch, completed, setError]);
248 const values = useDeepEqualReference({
249 isDefaultState,
250 initializing,
251 construct,
252 unconstruct,
253 constructed,
254 canSign,
255 generateAndSign,
256 signed,
257 broadcast,
258 broadcasted,
259 confirmed,
260 completed,
261 reset,
262 error,
263 inputsLocked,
264 //
265 txHash,
266 signedTransaction,
267 gasPrice,
268 setGasPrice,
269 resetGasPrice,
270 nonce,
271 chainId,
272 needFunds,
273 });
275 return { ...values, bind: values };