on 4a210ca help sign in
bridge/src/lib/useEthereumTransaction.js
372 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 //TODO this is duplicated across relevant callsites, should refactor
123 const txnSigner =
124 walletType === WALLET_TYPES.WALLET_CONNECT ? wcSign : undefined;
125 const txnSender =
126 walletType === WALLET_TYPES.WALLET_CONNECT ? wcSend : undefined;
128 const txns = await Promise.all(
129 utxs.map((utx, i) =>
130 signTransaction({
131 wallet: _wallet,
132 walletType,
133 walletHdPath,
134 networkType,
135 txn: utx,
136 nonce: nonce + i,
137 chainId,
138 gasPrice: gasPrice.toFixed(0),
139 gasLimit,
140 txnSigner,
141 txnSender,
142 })
143 )
144 );
146 setSignedTransactions(txns);
148 // Per return type of `signTransaction`, tx is either a Transaction or FakeSignedTx.
149 // We check the negation in this case, since TS cannot perform run-time check of type
150 // FakeSignedTx, but it can check for an instance of the ethereumjs/tx class.
151 if (txns.some(tx => !(tx instanceof Transaction))) {
152 setState(STATE.FAKE_SIGNED);
153 } else {
154 setState(STATE.SIGNED);
155 }
156 } catch (error) {
157 console.error('error signing txs', error);
158 setError(error);
159 }
160 }, [
161 _wallet,
162 chainId,
163 gasLimit,
164 gasPrice,
165 networkType,
166 nonce,
167 setError,
168 unsignedTransactions,
169 walletHdPath,
170 walletType,
171 wcSign,
172 wcSend,
173 ]);
175 const broadcast = useCallback(async () => {
176 try {
177 setConfirmationProgress(0.0);
178 setError(undefined);
179 setState(STATE.BROADCASTED);
181 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
183 const costGwei = toBN(gasLimit)
184 .mul(toBN(gasPrice))
185 .mul(toBN(rawTxs.length));
186 const cost = safeToWei(costGwei, 'gwei');
187 let usedTank = false;
188 // if this ethereum transaction is being executed by a specific point
189 // see if we can use the tank
190 if (Just.hasInstance(pointCursor)) {
191 usedTank = await ensureFundsFor(
192 _web3,
193 pointCursor.value,
194 _wallet.address,
195 walletType,
196 cost,
197 rawTxs,
198 (address, minBalance, balance) =>
199 setNeedFunds({ address, minBalance, balance }),
200 () => setNeedFunds(undefined)
201 );
202 }
204 const txHashes = await signedTransactions.reduce(
205 (acc, stx) =>
206 acc.then(hashes =>
207 sendSignedTransaction(
208 _web3,
209 stx,
210 /* doubtNonceError */ usedTank
211 ).then(hash => [...hashes, hash])
212 ),
213 Promise.resolve([])
214 );
216 setTxHashes(txHashes);
218 await timeout(PROGRESS_ANIMATION_DELAY_MS);
220 setConfirmationProgress(0.2);
222 const receipts = await Promise.all(
223 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
224 );
226 setFinalCost(
227 safeFromWei(
228 safeToWei(
229 receipts.reduce(
230 (acc, receipt) =>
231 toBN(receipt.gasUsed)
232 .mul(toBN(gasPrice))
233 .add(acc),
234 toBN(0)
235 ),
236 'gwei'
237 ),
238 'ether'
239 )
240 );
242 setConfirmationProgress(0.9);
244 setState(STATE.CONFIRMED);
245 } catch (error) {
246 setError(error);
247 }
248 }, [
249 _wallet.address,
250 _web3,
251 gasLimit,
252 gasPrice,
253 pointCursor,
254 setError,
255 signedTransactions,
256 walletType,
257 ]);
259 const reset = useCallback(() => {
260 setTxHashes(undefined);
261 setSignedTransactions(undefined);
262 setNonce(undefined);
263 setChainId(undefined);
264 resetGasPrice();
265 setState(STATE.NONE);
266 setError(undefined);
267 setNeedFunds(undefined);
268 setConfirmationProgress(0.0);
269 setFinalCost(undefined);
270 }, [resetGasPrice, setError]);
272 useEffect(() => {
273 let mounted = true;
274 // if nonce or chainId is undefined, re-fetch on-chain info
275 if (!(nonce === undefined || chainId === undefined)) {
276 return;
277 }
279 (async () => {
280 try {
281 setError(undefined);
282 const [nonce, chainId] = await Promise.all([
283 _web3.eth.getTransactionCount(_wallet.address),
284 _web3.eth.net.getId(),
285 ]);
287 if (!mounted) {
288 return;
289 }
291 setNonce(nonce);
292 setChainId(chainId);
293 } catch (error) {
294 setError(error);
295 }
296 })();
298 return () => (mounted = false);
299 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
301 useEffect(() => {
302 let mounted = true;
304 if (confirmed) {
305 (async () => {
306 if (refetch) {
307 try {
308 await refetch();
309 } catch (error) {
310 // log the original
311 console.error(error);
312 // track a user-friendly error
313 setError(
314 new Error(
315 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
316 )
317 );
318 }
319 }
321 if (!mounted) {
322 return;
323 }
325 setConfirmationProgress(1.0);
327 await timeout(PROGRESS_ANIMATION_DELAY_MS);
329 if (!mounted) {
330 return;
331 }
333 setState(STATE.COMPLETED);
334 })();
335 }
337 return () => (mounted = false);
338 }, [confirmed, refetch, completed, setError]);
340 const values = useDeepEqualReference({
341 isDefaultState,
342 initializing,
343 construct,
344 unconstruct,
345 constructed,
346 canSign,
347 generateAndSign,
348 signed,
349 fakeSigned,
350 broadcast,
351 broadcasted,
352 confirmed,
353 completed,
354 reset,
355 error,
356 inputsLocked,
357 confirmationProgress,
358 txHashes,
359 signedTransactions,
360 gasPrice,
361 setGasPrice,
362 resetGasPrice,
363 nonce,
364 chainId,
365 needFunds,
366 gasLimit,
367 unsignedTransactions,
368 finalCost,
369 });
371 return { ...values, bind: values };