on master help sign in
bridge/src/lib/useEthereumTransaction.js
368 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 { safeFromWei, gweiToWei } 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 cost = toBN(gasLimit)
185 .mul(gweiToWei(gasPrice.price))
186 .mul(toBN(rawTxs.length));
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 receipts.reduce(
229 (acc, receipt) =>
230 toBN(receipt.gasUsed)
231 .mul(gweiToWei(gasPrice.price))
232 .add(acc),
233 toBN(0)
234 ),
235 'ether'
236 )
237 );
239 setConfirmationProgress(0.9);
241 setState(STATE.CONFIRMED);
242 } catch (error) {
243 setError(error);
244 }
245 }, [
246 _wallet.address,
247 _web3,
248 gasLimit,
249 gasPrice,
250 pointCursor,
251 setError,
252 signedTransactions,
253 walletType,
254 ]);
256 const reset = useCallback(() => {
257 setTxHashes(undefined);
258 setSignedTransactions(undefined);
259 setNonce(undefined);
260 setChainId(undefined);
261 resetGasPrice();
262 setState(STATE.NONE);
263 setError(undefined);
264 setNeedFunds(undefined);
265 setConfirmationProgress(0.0);
266 setFinalCost(undefined);
267 }, [resetGasPrice, setError]);
269 useEffect(() => {
270 let mounted = true;
271 // if nonce or chainId is undefined, re-fetch on-chain info
272 if (!(nonce === undefined || chainId === undefined)) {
273 return;
274 }
276 (async () => {
277 try {
278 setError(undefined);
279 const [nonce, chainId] = await Promise.all([
280 _web3.eth.getTransactionCount(_wallet.address),
281 _web3.eth.getChainId(),
282 ]);
284 if (!mounted) {
285 return;
286 }
287 setNonce(nonce);
288 setChainId(chainId);
289 } catch (error) {
290 setError(error);
291 }
292 })();
294 return () => (mounted = false);
295 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
297 useEffect(() => {
298 let mounted = true;
300 if (confirmed) {
301 (async () => {
302 if (refetch) {
303 try {
304 await refetch();
305 } catch (error) {
306 // log the original
307 console.error(error);
308 // track a user-friendly error
309 setError(
310 new Error(
311 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
312 )
313 );
314 }
315 }
317 if (!mounted) {
318 return;
319 }
321 setConfirmationProgress(1.0);
323 await timeout(PROGRESS_ANIMATION_DELAY_MS);
325 if (!mounted) {
326 return;
327 }
329 setState(STATE.COMPLETED);
330 })();
331 }
333 return () => (mounted = false);
334 }, [confirmed, refetch, completed, setError]);
336 const values = useDeepEqualReference({
337 isDefaultState,
338 initializing,
339 construct,
340 unconstruct,
341 constructed,
342 canSign,
343 generateAndSign,
344 signed,
345 fakeSigned,
346 broadcast,
347 broadcasted,
348 confirmed,
349 completed,
350 reset,
351 error,
352 inputsLocked,
353 confirmationProgress,
354 txHashes,
355 signedTransactions,
356 gasPrice,
357 setGasPrice,
358 resetGasPrice,
359 nonce,
360 chainId,
361 needFunds,
362 gasLimit,
363 unsignedTransactions,
364 finalCost,
365 });
367 return { ...values, bind: values };