on 4a2b629 help sign in
bridge/src/lib/useEthereumTransaction.js
340 lines 8.6 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} from './constants';
14import {
15 signTransaction,
16 sendSignedTransaction,
17 waitForTransactionConfirm,
18 hexify,
19} from './txn';
20import * as need from 'lib/need';
21import { ensureFundsFor } from 'lib/tank';
22import useDeepEqualReference from 'lib/useDeepEqualReference';
23import useGasPrice from 'lib/useGasPrice';
24import timeout from 'lib/timeout';
25import { safeToWei, safeFromWei } from 'lib/lib';
27const STATE = {
28 NONE: 'NONE',
29 SIGNED: 'SIGNED',
30 BROADCASTED: 'BROADCASTED',
31 CONFIRMED: 'CONFIRMED',
32 COMPLETED: 'COMPLETED',
33};
35/**
36 * manage the state around sending and confirming an ethereum transaction
37 * @param {Transaction Function()} transactionBuilder
38 * @param {Promise<any> Function()} refetch async function called after completion
39 * @param {number} initialGasLimit gas limit
40 * @param {number} initialGasPrice gas price in gwei
41 */
42export default function useEthereumTransaction(
43 transactionBuilder,
44 refetch,
45 initialGasLimit = GAS_LIMITS.DEFAULT,
46 initialGasPrice = DEFAULT_GAS_PRICE_GWEI
47) {
48 const { wallet, walletType, walletHdPath } = useWallet();
49 const { web3, networkType } = useNetwork();
50 const { pointCursor } = usePointCursor();
52 const _web3 = need.web3(web3);
53 const _wallet = need.wallet(wallet);
55 const [error, _setError] = useState();
56 const setError = useCallback(
57 error => {
58 _setError(error);
59 if (error) {
60 console.error(error);
61 }
62 },
63 [_setError]
64 );
65 const [state, setState] = useState(STATE.NONE);
66 const [chainId, setChainId] = useState();
67 const [nonce, setNonce] = useState();
68 const { gasPrice, setGasPrice, resetGasPrice } = useGasPrice(initialGasPrice);
69 const [gasLimit] = useState(initialGasLimit);
70 const [unsignedTransactions, setUnsignedTransactions] = useState();
71 const [signedTransactions, setSignedTransactions] = useState();
72 const [txHashes, setTxHashes] = useState();
73 const [needFunds, setNeedFunds] = useState();
74 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
76 const [finalCost, setFinalCost] = useState();
78 const initializing = nonce === undefined || chainId === undefined;
79 const constructed = !!unsignedTransactions;
80 const isDefaultState = state === STATE.NONE;
81 const signed = state === STATE.SIGNED;
82 const broadcasted = state === STATE.BROADCASTED;
83 const confirmed = state === STATE.CONFIRMED;
84 const completed = state === STATE.COMPLETED;
86 // lock inputs once we're out of the default state
87 const inputsLocked = !isDefaultState;
89 // we can sign when:
90 const canSign = !initializing && constructed && isDefaultState;
92 const construct = useCallback(
93 async (...args) =>
94 setUnsignedTransactions(await transactionBuilder(...args)),
95 [transactionBuilder]
96 );
98 const unconstruct = useCallback(() => setUnsignedTransactions(undefined), [
99 setUnsignedTransactions,
100 ]);
102 const generateAndSign = useCallback(async () => {
103 try {
104 setError(undefined);
106 const utxs = Array.isArray(unsignedTransactions)
107 ? unsignedTransactions
108 : [unsignedTransactions];
110 const txns = await Promise.all(
111 utxs.map((utx, i) =>
112 signTransaction({
113 wallet: _wallet,
114 walletType,
115 walletHdPath,
116 networkType,
117 txn: utx,
118 nonce: nonce + i,
119 chainId,
120 gasPrice: gasPrice.toFixed(0),
121 gasLimit,
122 })
123 )
124 );
126 setSignedTransactions(txns);
127 setState(STATE.SIGNED);
128 } catch (error) {
129 setError(error);
130 }
131 }, [
132 _wallet,
133 chainId,
134 gasLimit,
135 gasPrice,
136 networkType,
137 nonce,
138 setError,
139 unsignedTransactions,
140 walletHdPath,
141 walletType,
142 ]);
144 const broadcast = useCallback(async () => {
145 try {
146 setConfirmationProgress(0.0);
147 setError(undefined);
149 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
151 const costGwei = toBN(gasLimit)
152 .mul(toBN(gasPrice))
153 .mul(toBN(rawTxs.length));
154 const cost = safeToWei(costGwei, 'gwei');
155 let usedTank = false;
156 // if this ethereum transaction is being executed by a specific point
157 // see if we can use the tank
158 if (Just.hasInstance(pointCursor)) {
159 usedTank = await ensureFundsFor(
160 _web3,
161 pointCursor.value,
162 _wallet.address,
163 walletType,
164 cost,
165 rawTxs,
166 (address, minBalance, balance) =>
167 setNeedFunds({ address, minBalance, balance }),
168 () => setNeedFunds(undefined)
169 );
170 }
172 const txHashes = await signedTransactions.reduce(
173 (acc, stx) =>
174 acc.then(hashes =>
175 sendSignedTransaction(
176 _web3,
177 stx,
178 /* doubtNonceError */ usedTank
179 ).then(hash => [...hashes, hash])
180 ),
181 Promise.resolve([])
182 );
184 setTxHashes(txHashes);
185 setState(STATE.BROADCASTED);
187 await timeout(PROGRESS_ANIMATION_DELAY_MS);
189 setConfirmationProgress(0.2);
191 const receipts = await Promise.all(
192 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
193 );
195 setFinalCost(
196 safeFromWei(
197 safeToWei(
198 receipts.reduce(
199 (acc, receipt) =>
200 toBN(receipt.gasUsed)
201 .mul(toBN(gasPrice))
202 .add(acc),
203 toBN(0)
204 ),
205 'gwei'
206 ),
207 'ether'
208 )
209 );
211 setConfirmationProgress(0.9);
213 setState(STATE.CONFIRMED);
214 } catch (error) {
215 setError(error);
216 }
217 }, [
218 _wallet.address,
219 _web3,
220 gasLimit,
221 gasPrice,
222 pointCursor,
223 setError,
224 signedTransactions,
225 walletType,
226 ]);
228 const reset = useCallback(() => {
229 setTxHashes(undefined);
230 setSignedTransactions(undefined);
231 setNonce(undefined);
232 setChainId(undefined);
233 resetGasPrice();
234 setState(STATE.NONE);
235 setError(undefined);
236 setNeedFunds(undefined);
237 setConfirmationProgress(0.0);
238 setFinalCost(undefined);
239 }, [resetGasPrice, setError]);
241 useEffect(() => {
242 let mounted = true;
243 // if nonce or chainId is undefined, re-fetch on-chain info
244 if (!(nonce === undefined || chainId === undefined)) {
245 return;
246 }
248 (async () => {
249 try {
250 setError(undefined);
251 const [nonce, chainId] = await Promise.all([
252 _web3.eth.getTransactionCount(_wallet.address),
253 _web3.eth.net.getId(),
254 ]);
256 if (!mounted) {
257 return;
258 }
260 setNonce(nonce);
261 setChainId(chainId);
262 } catch (error) {
263 setError(error);
264 }
265 })();
267 return () => (mounted = false);
268 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
270 useEffect(() => {
271 let mounted = true;
273 if (confirmed) {
274 (async () => {
275 if (refetch) {
276 try {
277 await refetch();
278 } catch (error) {
279 // log the original
280 console.error(error);
281 // track a user-friendly error
282 setError(
283 new Error(
284 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
285 )
286 );
287 }
288 }
290 if (!mounted) {
291 return;
292 }
294 setConfirmationProgress(1.0);
296 await timeout(PROGRESS_ANIMATION_DELAY_MS);
298 if (!mounted) {
299 return;
300 }
302 setState(STATE.COMPLETED);
303 })();
304 }
306 return () => (mounted = false);
307 }, [confirmed, refetch, completed, setError]);
309 const values = useDeepEqualReference({
310 isDefaultState,
311 initializing,
312 construct,
313 unconstruct,
314 constructed,
315 canSign,
316 generateAndSign,
317 signed,
318 broadcast,
319 broadcasted,
320 confirmed,
321 completed,
322 reset,
323 error,
324 inputsLocked,
325 confirmationProgress,
326 txHashes,
327 signedTransactions,
328 gasPrice,
329 setGasPrice,
330 resetGasPrice,
331 nonce,
332 chainId,
333 needFunds,
334 gasLimit,
335 unsignedTransactions,
336 finalCost,
337 });
339 return { ...values, bind: values };