on c250bda help sign in
bridge/src/lib/useEthereumTransaction.js
376 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 gasPrice: gasPrice.toFixed(0),
139 gasLimit,
140 txnSigner,
141 txnSender,
142 })
143 )
144 );
146 setSignedTransactions(txns);
147 console.log(2, 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 console.log(3)
155 } else {
156 setState(STATE.SIGNED);
157 console.log(4)
158 }
159 } catch (error) {
160 console.error('error signing txs', error);
161 setError(error);
162 }
163 }, [
164 _wallet,
165 chainId,
166 gasLimit,
167 gasPrice,
168 networkType,
169 nonce,
170 setError,
171 unsignedTransactions,
172 walletHdPath,
173 walletType,
174 wcSign,
175 wcSend,
176 ]);
178 const broadcast = useCallback(async () => {
179 try {
180 console.log(5)
181 setConfirmationProgress(0.0);
182 setError(undefined);
183 setState(STATE.BROADCASTED);
185 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
187 const costGwei = toBN(gasLimit)
188 .mul(toBN(gasPrice))
189 .mul(toBN(rawTxs.length));
190 const cost = safeToWei(costGwei, 'gwei');
191 let usedTank = false;
192 // if this ethereum transaction is being executed by a specific point
193 // see if we can use the tank
194 if (Just.hasInstance(pointCursor)) {
195 usedTank = await ensureFundsFor(
196 _web3,
197 pointCursor.value,
198 _wallet.address,
199 walletType,
200 cost,
201 rawTxs,
202 (address, minBalance, balance) =>
203 setNeedFunds({ address, minBalance, balance }),
204 () => setNeedFunds(undefined)
205 );
206 }
208 const txHashes = await signedTransactions.reduce(
209 (acc, stx) =>
210 acc.then(hashes =>
211 sendSignedTransaction(
212 _web3,
213 stx,
214 /* doubtNonceError */ usedTank
215 ).then(hash => [...hashes, hash])
216 ),
217 Promise.resolve([])
218 );
220 setTxHashes(txHashes);
222 await timeout(PROGRESS_ANIMATION_DELAY_MS);
224 setConfirmationProgress(0.2);
226 const receipts = await Promise.all(
227 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
228 );
230 setFinalCost(
231 safeFromWei(
232 safeToWei(
233 receipts.reduce(
234 (acc, receipt) =>
235 toBN(receipt.gasUsed)
236 .mul(toBN(gasPrice))
237 .add(acc),
238 toBN(0)
239 ),
240 'gwei'
241 ),
242 'ether'
243 )
244 );
246 setConfirmationProgress(0.9);
248 setState(STATE.CONFIRMED);
249 } catch (error) {
250 setError(error);
251 }
252 }, [
253 _wallet.address,
254 _web3,
255 gasLimit,
256 gasPrice,
257 pointCursor,
258 setError,
259 signedTransactions,
260 walletType,
261 ]);
263 const reset = useCallback(() => {
264 setTxHashes(undefined);
265 setSignedTransactions(undefined);
266 setNonce(undefined);
267 setChainId(undefined);
268 resetGasPrice();
269 setState(STATE.NONE);
270 setError(undefined);
271 setNeedFunds(undefined);
272 setConfirmationProgress(0.0);
273 setFinalCost(undefined);
274 }, [resetGasPrice, setError]);
276 useEffect(() => {
277 let mounted = true;
278 // if nonce or chainId is undefined, re-fetch on-chain info
279 if (!(nonce === undefined || chainId === undefined)) {
280 return;
281 }
283 (async () => {
284 try {
285 setError(undefined);
286 const [nonce, chainId] = await Promise.all([
287 _web3.eth.getTransactionCount(_wallet.address),
288 _web3.eth.net.getId(),
289 ]);
291 if (!mounted) {
292 return;
293 }
295 setNonce(nonce);
296 setChainId(chainId);
297 } catch (error) {
298 setError(error);
299 }
300 })();
302 return () => (mounted = false);
303 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
305 useEffect(() => {
306 let mounted = true;
308 if (confirmed) {
309 (async () => {
310 if (refetch) {
311 try {
312 await refetch();
313 } catch (error) {
314 // log the original
315 console.error(error);
316 // track a user-friendly error
317 setError(
318 new Error(
319 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
320 )
321 );
322 }
323 }
325 if (!mounted) {
326 return;
327 }
329 setConfirmationProgress(1.0);
331 await timeout(PROGRESS_ANIMATION_DELAY_MS);
333 if (!mounted) {
334 return;
335 }
337 setState(STATE.COMPLETED);
338 })();
339 }
341 return () => (mounted = false);
342 }, [confirmed, refetch, completed, setError]);
344 const values = useDeepEqualReference({
345 isDefaultState,
346 initializing,
347 construct,
348 unconstruct,
349 constructed,
350 canSign,
351 generateAndSign,
352 signed,
353 fakeSigned,
354 broadcast,
355 broadcasted,
356 confirmed,
357 completed,
358 reset,
359 error,
360 inputsLocked,
361 confirmationProgress,
362 txHashes,
363 signedTransactions,
364 gasPrice,
365 setGasPrice,
366 resetGasPrice,
367 nonce,
368 chainId,
369 needFunds,
370 gasLimit,
371 unsignedTransactions,
372 finalCost,
373 });
375 return { ...values, bind: values };