on 86fddd6 help sign in
bridge/src/lib/useEthereumTransaction.js
352 lines 9.0 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';
29const STATE = {
30 NONE: 'NONE',
31 SIGNED: 'SIGNED',
32 BROADCASTED: 'BROADCASTED',
33 CONFIRMED: 'CONFIRMED',
34 COMPLETED: 'COMPLETED',
35};
37/**
38 * manage the state around sending and confirming an ethereum transaction
39 * @param {Transaction Function()} transactionBuilder
40 * @param {Promise<any> Function()} refetch async function called after completion
41 * @param {number} initialGasLimit gas limit
42 * @param {number} initialGasPrice gas price in gwei
43 */
44export default function useEthereumTransaction(
45 transactionBuilder,
46 refetch,
47 initialGasLimit = GAS_LIMITS.DEFAULT,
48 initialGasPrice = DEFAULT_GAS_PRICE_GWEI
49) {
50 const { wallet, walletType, walletHdPath } = useWallet();
51 const { web3, networkType } = useNetwork();
52 const { pointCursor } = usePointCursor();
53 const { signTransaction: wcSign } = useWalletConnect();
55 const _web3 = need.web3(web3);
56 const _wallet = need.wallet(wallet);
58 const [error, _setError] = useState();
59 const setError = useCallback(
60 error => {
61 _setError(error);
62 if (error) {
63 console.error(error);
64 }
65 },
66 [_setError]
67 );
68 const [state, setState] = useState(STATE.NONE);
69 const [chainId, setChainId] = useState();
70 const [nonce, setNonce] = useState();
71 const { gasPrice, setGasPrice, resetGasPrice } = useGasPrice(initialGasPrice);
72 const [gasLimit] = useState(initialGasLimit);
73 const [unsignedTransactions, setUnsignedTransactions] = useState();
74 const [signedTransactions, setSignedTransactions] = useState();
75 const [txHashes, setTxHashes] = useState();
76 const [needFunds, setNeedFunds] = useState();
77 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
79 const [finalCost, setFinalCost] = useState();
81 const initializing = nonce === undefined || chainId === undefined;
82 const constructed = !!unsignedTransactions;
83 const isDefaultState = state === STATE.NONE;
84 const signed = state === STATE.SIGNED;
85 const broadcasted = state === STATE.BROADCASTED;
86 const confirmed = state === STATE.CONFIRMED;
87 const completed = state === STATE.COMPLETED;
89 // lock inputs once we're out of the default state
90 const inputsLocked = !isDefaultState;
92 // we can sign when:
93 const canSign = !initializing && constructed && isDefaultState;
95 const construct = useCallback(
96 async (...args) =>
97 setUnsignedTransactions(await transactionBuilder(...args)),
98 [transactionBuilder]
99 );
101 const unconstruct = useCallback(() => setUnsignedTransactions(undefined), [
102 setUnsignedTransactions,
103 ]);
105 const generateAndSign = useCallback(async () => {
106 try {
107 setError(undefined);
109 const utxs = Array.isArray(unsignedTransactions)
110 ? unsignedTransactions
111 : [unsignedTransactions];
113 // Due to React's Rules of Hooks, optionally inject the
114 // WC transaction signing function
115 // https://reactjs.org/docs/hooks-rules.html
116 const txnSigner =
117 walletType === WALLET_TYPES.WALLET_CONNECT ? wcSign : undefined;
119 const txns = await Promise.all(
120 utxs.map((utx, i) =>
121 signTransaction({
122 wallet: _wallet,
123 walletType,
124 walletHdPath,
125 networkType,
126 txn: utx,
127 nonce: nonce + i,
128 chainId,
129 gasPrice: gasPrice.toFixed(0),
130 gasLimit,
131 txnSigner,
132 })
133 )
134 );
136 setSignedTransactions(txns);
137 setState(STATE.SIGNED);
138 } catch (error) {
139 console.error('error signing txs', error);
140 setError(error);
141 }
142 }, [
143 _wallet,
144 chainId,
145 gasLimit,
146 gasPrice,
147 networkType,
148 nonce,
149 setError,
150 unsignedTransactions,
151 walletHdPath,
152 walletType,
153 wcSign,
154 ]);
156 const broadcast = useCallback(async () => {
157 try {
158 setConfirmationProgress(0.0);
159 setError(undefined);
160 setState(STATE.BROADCASTED);
162 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
164 const costGwei = toBN(gasLimit)
165 .mul(toBN(gasPrice))
166 .mul(toBN(rawTxs.length));
167 const cost = safeToWei(costGwei, 'gwei');
168 let usedTank = false;
169 // if this ethereum transaction is being executed by a specific point
170 // see if we can use the tank
171 if (Just.hasInstance(pointCursor)) {
172 usedTank = await ensureFundsFor(
173 _web3,
174 pointCursor.value,
175 _wallet.address,
176 walletType,
177 cost,
178 rawTxs,
179 (address, minBalance, balance) =>
180 setNeedFunds({ address, minBalance, balance }),
181 () => setNeedFunds(undefined)
182 );
183 }
185 const txHashes = await signedTransactions.reduce(
186 (acc, stx) =>
187 acc.then(hashes =>
188 sendSignedTransaction(
189 _web3,
190 stx,
191 /* doubtNonceError */ usedTank
192 ).then(hash => [...hashes, hash])
193 ),
194 Promise.resolve([])
195 );
197 setTxHashes(txHashes);
199 await timeout(PROGRESS_ANIMATION_DELAY_MS);
201 setConfirmationProgress(0.2);
203 const receipts = await Promise.all(
204 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
205 );
207 setFinalCost(
208 safeFromWei(
209 safeToWei(
210 receipts.reduce(
211 (acc, receipt) =>
212 toBN(receipt.gasUsed)
213 .mul(toBN(gasPrice))
214 .add(acc),
215 toBN(0)
216 ),
217 'gwei'
218 ),
219 'ether'
220 )
221 );
223 setConfirmationProgress(0.9);
225 setState(STATE.CONFIRMED);
226 } catch (error) {
227 setError(error);
228 }
229 }, [
230 _wallet.address,
231 _web3,
232 gasLimit,
233 gasPrice,
234 pointCursor,
235 setError,
236 signedTransactions,
237 walletType,
238 ]);
240 const reset = useCallback(() => {
241 setTxHashes(undefined);
242 setSignedTransactions(undefined);
243 setNonce(undefined);
244 setChainId(undefined);
245 resetGasPrice();
246 setState(STATE.NONE);
247 setError(undefined);
248 setNeedFunds(undefined);
249 setConfirmationProgress(0.0);
250 setFinalCost(undefined);
251 }, [resetGasPrice, setError]);
253 useEffect(() => {
254 let mounted = true;
255 // if nonce or chainId is undefined, re-fetch on-chain info
256 if (!(nonce === undefined || chainId === undefined)) {
257 return;
258 }
260 (async () => {
261 try {
262 setError(undefined);
263 const [nonce, chainId] = await Promise.all([
264 _web3.eth.getTransactionCount(_wallet.address),
265 _web3.eth.net.getId(),
266 ]);
268 if (!mounted) {
269 return;
270 }
272 setNonce(nonce);
273 setChainId(chainId);
274 } catch (error) {
275 setError(error);
276 }
277 })();
279 return () => (mounted = false);
280 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
282 useEffect(() => {
283 let mounted = true;
285 if (confirmed) {
286 (async () => {
287 if (refetch) {
288 try {
289 await refetch();
290 } catch (error) {
291 // log the original
292 console.error(error);
293 // track a user-friendly error
294 setError(
295 new Error(
296 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
297 )
298 );
299 }
300 }
302 if (!mounted) {
303 return;
304 }
306 setConfirmationProgress(1.0);
308 await timeout(PROGRESS_ANIMATION_DELAY_MS);
310 if (!mounted) {
311 return;
312 }
314 setState(STATE.COMPLETED);
315 })();
316 }
318 return () => (mounted = false);
319 }, [confirmed, refetch, completed, setError]);
321 const values = useDeepEqualReference({
322 isDefaultState,
323 initializing,
324 construct,
325 unconstruct,
326 constructed,
327 canSign,
328 generateAndSign,
329 signed,
330 broadcast,
331 broadcasted,
332 confirmed,
333 completed,
334 reset,
335 error,
336 inputsLocked,
337 confirmationProgress,
338 txHashes,
339 signedTransactions,
340 gasPrice,
341 setGasPrice,
342 resetGasPrice,
343 nonce,
344 chainId,
345 needFunds,
346 gasLimit,
347 unsignedTransactions,
348 finalCost,
349 });
351 return { ...values, bind: values };