on 703ff86 help sign in
bridge/src/lib/useEthereumTransaction.js
372 lines 9.9 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 gasPriceData: gasPrice,
139 gasLimit,
140 txnSigner,
141 txnSender,
142 web3: _web3,
143 })
144 )
145 );
147 setSignedTransactions(txns);
149 // Per return type of `signTransaction`, tx is either a Transaction or FakeSignedTx.
150 // We check the negation in this case, since TS cannot perform run-time check of type
151 // FakeSignedTx, but it can check for an instance of the ethereumjs/tx class.
152 if (txns.some(tx => !(tx instanceof Transaction))) {
153 setState(STATE.FAKE_SIGNED);
154 } else {
155 setState(STATE.SIGNED);
156 }
157 } catch (error) {
158 console.error('error signing txs', error);
159 setError(error);
160 }
161 }, [
162 _wallet,
163 chainId,
164 gasLimit,
165 gasPrice,
166 networkType,
167 nonce,
168 setError,
169 unsignedTransactions,
170 walletHdPath,
171 walletType,
172 wcSign,
173 wcSend,
174 ]);
176 const broadcast = useCallback(async () => {
177 try {
178 setConfirmationProgress(0.0);
179 setError(undefined);
180 setState(STATE.BROADCASTED);
182 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
184 const costGwei = toBN(gasLimit)
185 .mul(toBN(gasPrice.price))
186 .mul(toBN(rawTxs.length));
187 const cost = safeToWei(costGwei, 'gwei');
188 let usedTank = false;
189 // if this ethereum transaction is being executed by a specific point
190 // see if we can use the tank
191 if (Just.hasInstance(pointCursor)) {
192 usedTank = await ensureFundsFor(
193 _web3,
194 pointCursor.value,
195 _wallet.address,
196 walletType,
197 cost,
198 rawTxs,
199 (address, minBalance, balance) =>
200 setNeedFunds({ address, minBalance, balance }),
201 () => setNeedFunds(undefined)
202 );
203 }
205 const txHashes = await signedTransactions.reduce(
206 (acc, stx) =>
207 acc.then(hashes =>
208 sendSignedTransaction(
209 _web3,
210 stx,
211 /* doubtNonceError */ usedTank
212 ).then(hash => [...hashes, hash])
213 ),
214 Promise.resolve([])
215 );
217 setTxHashes(txHashes);
219 await timeout(PROGRESS_ANIMATION_DELAY_MS);
221 setConfirmationProgress(0.2);
223 const receipts = await Promise.all(
224 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
225 );
227 setFinalCost(
228 safeFromWei(
229 safeToWei(
230 receipts.reduce(
231 (acc, receipt) =>
232 toBN(receipt.gasUsed)
233 .mul(toBN(gasPrice.price))
234 .add(acc),
235 toBN(0)
236 ),
237 'gwei'
238 ),
239 'ether'
240 )
241 );
243 setConfirmationProgress(0.9);
245 setState(STATE.CONFIRMED);
246 } catch (error) {
247 setError(error);
248 }
249 }, [
250 _wallet.address,
251 _web3,
252 gasLimit,
253 gasPrice,
254 pointCursor,
255 setError,
256 signedTransactions,
257 walletType,
258 ]);
260 const reset = useCallback(() => {
261 setTxHashes(undefined);
262 setSignedTransactions(undefined);
263 setNonce(undefined);
264 setChainId(undefined);
265 resetGasPrice();
266 setState(STATE.NONE);
267 setError(undefined);
268 setNeedFunds(undefined);
269 setConfirmationProgress(0.0);
270 setFinalCost(undefined);
271 }, [resetGasPrice, setError]);
273 useEffect(() => {
274 let mounted = true;
275 // if nonce or chainId is undefined, re-fetch on-chain info
276 if (!(nonce === undefined || chainId === undefined)) {
277 return;
278 }
280 (async () => {
281 try {
282 setError(undefined);
283 const [nonce, chainId] = await Promise.all([
284 _web3.eth.getTransactionCount(_wallet.address),
285 _web3.eth.getChainId(),
286 ]);
288 if (!mounted) {
289 return;
290 }
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 };