on 7e11fce help sign in
bridge/src/lib/useEthereumTransaction.js
368 lines 9.5 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 FakeSignResult,
21} from './txn';
22import * as need from 'lib/need';
23import { ensureFundsFor } from 'lib/tank';
24import useDeepEqualReference from 'lib/useDeepEqualReference';
25import useGasPrice from 'lib/useGasPrice';
26import { timeout } from 'lib/timeout';
27import { safeToWei, safeFromWei } from 'lib/lib';
28import { useWalletConnect } from './useWalletConnect';
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 if (txns.some(tx => tx instanceof FakeSignResult)) {
148 setState(STATE.FAKE_SIGNED);
149 } else {
150 setState(STATE.SIGNED);
151 }
152 } catch (error) {
153 console.error('error signing txs', error);
154 setError(error);
155 }
156 }, [
157 _wallet,
158 chainId,
159 gasLimit,
160 gasPrice,
161 networkType,
162 nonce,
163 setError,
164 unsignedTransactions,
165 walletHdPath,
166 walletType,
167 wcSign,
168 wcSend,
169 ]);
171 const broadcast = useCallback(async () => {
172 try {
173 setConfirmationProgress(0.0);
174 setError(undefined);
175 setState(STATE.BROADCASTED);
177 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
179 const costGwei = toBN(gasLimit)
180 .mul(toBN(gasPrice))
181 .mul(toBN(rawTxs.length));
182 const cost = safeToWei(costGwei, 'gwei');
183 let usedTank = false;
184 // if this ethereum transaction is being executed by a specific point
185 // see if we can use the tank
186 if (Just.hasInstance(pointCursor)) {
187 usedTank = await ensureFundsFor(
188 _web3,
189 pointCursor.value,
190 _wallet.address,
191 walletType,
192 cost,
193 rawTxs,
194 (address, minBalance, balance) =>
195 setNeedFunds({ address, minBalance, balance }),
196 () => setNeedFunds(undefined)
197 );
198 }
200 const txHashes = await signedTransactions.reduce(
201 (acc, stx) =>
202 acc.then(hashes =>
203 sendSignedTransaction(
204 _web3,
205 stx,
206 /* doubtNonceError */ usedTank
207 ).then(hash => [...hashes, hash])
208 ),
209 Promise.resolve([])
210 );
212 setTxHashes(txHashes);
214 await timeout(PROGRESS_ANIMATION_DELAY_MS);
216 setConfirmationProgress(0.2);
218 const receipts = await Promise.all(
219 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
220 );
222 setFinalCost(
223 safeFromWei(
224 safeToWei(
225 receipts.reduce(
226 (acc, receipt) =>
227 toBN(receipt.gasUsed)
228 .mul(toBN(gasPrice))
229 .add(acc),
230 toBN(0)
231 ),
232 'gwei'
233 ),
234 'ether'
235 )
236 );
238 setConfirmationProgress(0.9);
240 setState(STATE.CONFIRMED);
241 } catch (error) {
242 setError(error);
243 }
244 }, [
245 _wallet.address,
246 _web3,
247 gasLimit,
248 gasPrice,
249 pointCursor,
250 setError,
251 signedTransactions,
252 walletType,
253 ]);
255 const reset = useCallback(() => {
256 setTxHashes(undefined);
257 setSignedTransactions(undefined);
258 setNonce(undefined);
259 setChainId(undefined);
260 resetGasPrice();
261 setState(STATE.NONE);
262 setError(undefined);
263 setNeedFunds(undefined);
264 setConfirmationProgress(0.0);
265 setFinalCost(undefined);
266 }, [resetGasPrice, setError]);
268 useEffect(() => {
269 let mounted = true;
270 // if nonce or chainId is undefined, re-fetch on-chain info
271 if (!(nonce === undefined || chainId === undefined)) {
272 return;
273 }
275 (async () => {
276 try {
277 setError(undefined);
278 const [nonce, chainId] = await Promise.all([
279 _web3.eth.getTransactionCount(_wallet.address),
280 _web3.eth.net.getId(),
281 ]);
283 if (!mounted) {
284 return;
285 }
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 };