on 01a1446 help sign in
bridge/src/lib/useEthereumTransaction.js
371 lines 9.8 kB history raw
1import { useCallback, useEffect, useState } from 'react';
2import { Just } from 'folktale/maybe';
3import { toBN } from 'web3-utils';
5import { useNetwork } from 'store/network';
6import { useWallet } from 'store/wallet';
7import { usePointCursor } from 'store/pointCursor';
9import {
10 GAS_LIMITS,
11 DEFAULT_GAS_PRICE_GWEI,
12 PROGRESS_ANIMATION_DELAY_MS,
13 WALLET_TYPES,
14} from './constants';
15import {
16 signTransaction,
17 sendSignedTransaction,
18 waitForTransactionConfirm,
19 hexify,
20} from './txn';
21import * as need from 'lib/need';
22import { ensureFundsFor } from 'lib/tank';
23import useDeepEqualReference from 'lib/useDeepEqualReference';
24import useGasPrice from 'lib/useGasPrice';
25import { timeout } from 'lib/timeout';
26import { safeToWei, safeFromWei } from 'lib/lib';
27import { useWalletConnect } from './useWalletConnect';
28import { Transaction } from '@ethereumjs/tx';
30const STATE = {
31 NONE: 'NONE',
32 SIGNED: 'SIGNED',
33 FAKE_SIGNED: 'FAKE_SIGNED',
34 BROADCASTED: 'BROADCASTED',
35 CONFIRMED: 'CONFIRMED',
36 COMPLETED: 'COMPLETED',
37};
39/**
40 * manage the state around sending and confirming an ethereum transaction
41 * @param {Transaction Function()} transactionBuilder
42 * @param {Promise<any> Function()} refetch async function called after completion
43 * @param {number} initialGasLimit gas limit
44 * @param {number} initialGasPrice gas price in gwei
45 */
46export default function useEthereumTransaction(
47 transactionBuilder,
48 refetch,
49 initialGasLimit = GAS_LIMITS.DEFAULT,
50 initialGasPrice = DEFAULT_GAS_PRICE_GWEI
51) {
52 const { wallet, walletType, walletHdPath } = useWallet();
53 const { web3, networkType } = useNetwork();
54 const { pointCursor } = usePointCursor();
55 const {
56 signTransaction: wcSign,
57 sendTransaction: wcSend,
58 } = useWalletConnect();
60 const _web3 = need.web3(web3);
61 const _wallet = need.wallet(wallet);
63 const [error, _setError] = useState();
64 const setError = useCallback(
65 error => {
66 _setError(error);
67 if (error) {
68 console.error(error);
69 }
70 },
71 [_setError]
72 );
73 const [state, setState] = useState(STATE.NONE);
74 const [chainId, setChainId] = useState();
75 const [nonce, setNonce] = useState();
76 const { gasPrice, setGasPrice, resetGasPrice } = useGasPrice(initialGasPrice);
77 const [gasLimit] = useState(initialGasLimit);
78 const [unsignedTransactions, setUnsignedTransactions] = useState();
79 const [signedTransactions, setSignedTransactions] = useState();
80 const [txHashes, setTxHashes] = useState();
81 const [needFunds, setNeedFunds] = useState();
82 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
84 const [finalCost, setFinalCost] = useState();
86 const initializing = nonce === undefined || chainId === undefined;
87 const constructed = !!unsignedTransactions;
88 const isDefaultState = state === STATE.NONE;
89 const signed = state === STATE.SIGNED || state === STATE.FAKE_SIGNED;
90 const fakeSigned = state === STATE.FAKE_SIGNED;
91 const broadcasted = state === STATE.BROADCASTED;
92 const confirmed = state === STATE.CONFIRMED;
93 const completed = state === STATE.COMPLETED;
95 // lock inputs once we're out of the default state
96 const inputsLocked = !isDefaultState;
98 // we can sign when:
99 const canSign = !initializing && constructed && isDefaultState;
101 const construct = useCallback(
102 async (...args) =>
103 setUnsignedTransactions(await transactionBuilder(...args)),
104 [transactionBuilder]
105 );
107 const unconstruct = useCallback(() => setUnsignedTransactions(undefined), [
108 setUnsignedTransactions,
109 ]);
111 const generateAndSign = useCallback(async () => {
112 try {
113 setError(undefined);
115 const utxs = Array.isArray(unsignedTransactions)
116 ? unsignedTransactions
117 : [unsignedTransactions];
119 // Due to React's Rules of Hooks, optionally inject the
120 // WC transaction signing function
121 // https://reactjs.org/docs/hooks-rules.html
122 const txnSigner =
123 walletType === WALLET_TYPES.WALLET_CONNECT ? wcSign : undefined;
124 const txnSender =
125 walletType === WALLET_TYPES.WALLET_CONNECT ? wcSend : undefined;
127 const txns = await Promise.all(
128 utxs.map((utx, i) =>
129 signTransaction({
130 wallet: _wallet,
131 walletType,
132 walletHdPath,
133 networkType,
134 txn: utx,
135 nonce: nonce + i,
136 chainId,
137 gasPrice: gasPrice.toFixed(0),
138 gasLimit,
139 txnSigner,
140 txnSender,
141 })
142 )
143 );
145 setSignedTransactions(txns);
147 // Per return type of `signTransaction`, tx is either a Transaction or FakeSignedTx.
148 // We check the negation in this case, since TS cannot perform run-time check of type
149 // FakeSignedTx, but it can check for an instance of the ethereumjs/tx class.
150 if (txns.some(tx => !(tx instanceof Transaction))) {
151 setState(STATE.FAKE_SIGNED);
152 } else {
153 setState(STATE.SIGNED);
154 }
155 } catch (error) {
156 console.error('error signing txs', error);
157 setError(error);
158 }
159 }, [
160 _wallet,
161 chainId,
162 gasLimit,
163 gasPrice,
164 networkType,
165 nonce,
166 setError,
167 unsignedTransactions,
168 walletHdPath,
169 walletType,
170 wcSign,
171 wcSend,
172 ]);
174 const broadcast = useCallback(async () => {
175 try {
176 setConfirmationProgress(0.0);
177 setError(undefined);
178 setState(STATE.BROADCASTED);
180 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
182 const costGwei = toBN(gasLimit)
183 .mul(toBN(gasPrice))
184 .mul(toBN(rawTxs.length));
185 const cost = safeToWei(costGwei, 'gwei');
186 let usedTank = false;
187 // if this ethereum transaction is being executed by a specific point
188 // see if we can use the tank
189 if (Just.hasInstance(pointCursor)) {
190 usedTank = await ensureFundsFor(
191 _web3,
192 pointCursor.value,
193 _wallet.address,
194 walletType,
195 cost,
196 rawTxs,
197 (address, minBalance, balance) =>
198 setNeedFunds({ address, minBalance, balance }),
199 () => setNeedFunds(undefined)
200 );
201 }
203 const txHashes = await signedTransactions.reduce(
204 (acc, stx) =>
205 acc.then(hashes =>
206 sendSignedTransaction(
207 _web3,
208 stx,
209 /* doubtNonceError */ usedTank
210 ).then(hash => [...hashes, hash])
211 ),
212 Promise.resolve([])
213 );
215 setTxHashes(txHashes);
217 await timeout(PROGRESS_ANIMATION_DELAY_MS);
219 setConfirmationProgress(0.2);
221 const receipts = await Promise.all(
222 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
223 );
225 setFinalCost(
226 safeFromWei(
227 safeToWei(
228 receipts.reduce(
229 (acc, receipt) =>
230 toBN(receipt.gasUsed)
231 .mul(toBN(gasPrice))
232 .add(acc),
233 toBN(0)
234 ),
235 'gwei'
236 ),
237 'ether'
238 )
239 );
241 setConfirmationProgress(0.9);
243 setState(STATE.CONFIRMED);
244 } catch (error) {
245 setError(error);
246 }
247 }, [
248 _wallet.address,
249 _web3,
250 gasLimit,
251 gasPrice,
252 pointCursor,
253 setError,
254 signedTransactions,
255 walletType,
256 ]);
258 const reset = useCallback(() => {
259 setTxHashes(undefined);
260 setSignedTransactions(undefined);
261 setNonce(undefined);
262 setChainId(undefined);
263 resetGasPrice();
264 setState(STATE.NONE);
265 setError(undefined);
266 setNeedFunds(undefined);
267 setConfirmationProgress(0.0);
268 setFinalCost(undefined);
269 }, [resetGasPrice, setError]);
271 useEffect(() => {
272 let mounted = true;
273 // if nonce or chainId is undefined, re-fetch on-chain info
274 if (!(nonce === undefined || chainId === undefined)) {
275 return;
276 }
278 (async () => {
279 try {
280 setError(undefined);
281 const [nonce, chainId] = await Promise.all([
282 _web3.eth.getTransactionCount(_wallet.address),
283 _web3.eth.net.getId(),
284 ]);
286 if (!mounted) {
287 return;
288 }
290 setNonce(nonce);
291 setChainId(chainId);
292 } catch (error) {
293 setError(error);
294 }
295 })();
297 return () => (mounted = false);
298 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
300 useEffect(() => {
301 let mounted = true;
303 if (confirmed) {
304 (async () => {
305 if (refetch) {
306 try {
307 await refetch();
308 } catch (error) {
309 // log the original
310 console.error(error);
311 // track a user-friendly error
312 setError(
313 new Error(
314 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
315 )
316 );
317 }
318 }
320 if (!mounted) {
321 return;
322 }
324 setConfirmationProgress(1.0);
326 await timeout(PROGRESS_ANIMATION_DELAY_MS);
328 if (!mounted) {
329 return;
330 }
332 setState(STATE.COMPLETED);
333 })();
334 }
336 return () => (mounted = false);
337 }, [confirmed, refetch, completed, setError]);
339 const values = useDeepEqualReference({
340 isDefaultState,
341 initializing,
342 construct,
343 unconstruct,
344 constructed,
345 canSign,
346 generateAndSign,
347 signed,
348 fakeSigned,
349 broadcast,
350 broadcasted,
351 confirmed,
352 completed,
353 reset,
354 error,
355 inputsLocked,
356 confirmationProgress,
357 txHashes,
358 signedTransactions,
359 gasPrice,
360 setGasPrice,
361 resetGasPrice,
362 nonce,
363 chainId,
364 needFunds,
365 gasLimit,
366 unsignedTransactions,
367 finalCost,
368 });
370 return { ...values, bind: values };