on 5eceb4e 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);
148 setState(STATE.BROADCASTED);
150 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
152 const costGwei = toBN(gasLimit)
153 .mul(toBN(gasPrice))
154 .mul(toBN(rawTxs.length));
155 const cost = safeToWei(costGwei, 'gwei');
156 let usedTank = false;
157 // if this ethereum transaction is being executed by a specific point
158 // see if we can use the tank
159 if (Just.hasInstance(pointCursor)) {
160 usedTank = await ensureFundsFor(
161 _web3,
162 pointCursor.value,
163 _wallet.address,
164 walletType,
165 cost,
166 rawTxs,
167 (address, minBalance, balance) =>
168 setNeedFunds({ address, minBalance, balance }),
169 () => setNeedFunds(undefined)
170 );
171 }
173 const txHashes = await signedTransactions.reduce(
174 (acc, stx) =>
175 acc.then(hashes =>
176 sendSignedTransaction(
177 _web3,
178 stx,
179 /* doubtNonceError */ usedTank
180 ).then(hash => [...hashes, hash])
181 ),
182 Promise.resolve([])
183 );
185 setTxHashes(txHashes);
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 };