on 5ce8e92 help sign in
bridge/src/lib/useEthereumTransaction.js
317 lines 8.1 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 } 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 initializing = nonce === undefined || chainId === undefined;
77 const constructed = !!unsignedTransactions;
78 const isDefaultState = state === STATE.NONE;
79 const signed = state === STATE.SIGNED;
80 const broadcasted = state === STATE.BROADCASTED;
81 const confirmed = state === STATE.CONFIRMED;
82 const completed = state === STATE.COMPLETED;
84 // lock inputs once we're out of the default state
85 const inputsLocked = !isDefaultState;
87 // we can sign when:
88 const canSign = !initializing && constructed && isDefaultState;
90 const construct = useCallback(
91 async (...args) =>
92 setUnsignedTransactions(await transactionBuilder(...args)),
93 [transactionBuilder]
94 );
96 const unconstruct = useCallback(() => setUnsignedTransactions(undefined), [
97 setUnsignedTransactions,
98 ]);
100 const generateAndSign = useCallback(async () => {
101 try {
102 setError(undefined);
104 const utxs = Array.isArray(unsignedTransactions)
105 ? unsignedTransactions
106 : [unsignedTransactions];
108 const txns = await Promise.all(
109 utxs.map((utx, i) =>
110 signTransaction({
111 wallet: _wallet,
112 walletType,
113 walletHdPath,
114 networkType,
115 txn: utx,
116 nonce: nonce + i,
117 chainId,
118 gasPrice: gasPrice.toFixed(0),
119 gasLimit,
120 })
121 )
122 );
124 setSignedTransactions(txns);
125 setState(STATE.SIGNED);
126 } catch (error) {
127 setError(error);
128 }
129 }, [
130 _wallet,
131 chainId,
132 gasLimit,
133 gasPrice,
134 networkType,
135 nonce,
136 setError,
137 unsignedTransactions,
138 walletHdPath,
139 walletType,
140 ]);
142 const broadcast = useCallback(async () => {
143 try {
144 setConfirmationProgress(0.0);
145 setError(undefined);
147 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
149 const costGwei = toBN(gasLimit)
150 .mul(toBN(gasPrice))
151 .mul(toBN(rawTxs.length));
152 const cost = safeToWei(costGwei, 'gwei');
153 let usedTank = false;
154 // if this ethereum transaction is being executed by a specific point
155 // see if we can use the tank
156 if (Just.hasInstance(pointCursor)) {
157 usedTank = await ensureFundsFor(
158 _web3,
159 pointCursor.value,
160 _wallet.address,
161 walletType,
162 cost,
163 rawTxs,
164 (address, minBalance, balance) =>
165 setNeedFunds({ address, minBalance, balance }),
166 () => setNeedFunds(undefined)
167 );
168 }
170 const txHashes = await signedTransactions.reduce(
171 (acc, stx) =>
172 acc.then(hashes =>
173 sendSignedTransaction(
174 _web3,
175 stx,
176 /* doubtNonceError */ usedTank
177 ).then(hash => [...hashes, hash])
178 ),
179 Promise.resolve([])
180 );
182 setTxHashes(txHashes);
183 setState(STATE.BROADCASTED);
185 await timeout(PROGRESS_ANIMATION_DELAY_MS);
187 setConfirmationProgress(0.2);
189 await Promise.all(
190 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
191 );
193 setConfirmationProgress(0.9);
195 setState(STATE.CONFIRMED);
196 } catch (error) {
197 setError(error);
198 }
199 }, [
200 _wallet.address,
201 _web3,
202 gasLimit,
203 gasPrice,
204 pointCursor,
205 setError,
206 signedTransactions,
207 ]);
209 const reset = useCallback(() => {
210 setTxHashes(undefined);
211 setSignedTransactions(undefined);
212 setNonce(undefined);
213 setChainId(undefined);
214 resetGasPrice();
215 setState(STATE.NONE);
216 setError(undefined);
217 setNeedFunds(undefined);
218 setConfirmationProgress(0.0);
219 }, [resetGasPrice, setError]);
221 useEffect(() => {
222 let mounted = true;
223 // if nonce or chainId is undefined, re-fetch on-chain info
224 if (!(nonce === undefined || chainId === undefined)) {
225 return;
226 }
228 (async () => {
229 try {
230 setError(undefined);
231 const [nonce, chainId] = await Promise.all([
232 _web3.eth.getTransactionCount(_wallet.address),
233 _web3.eth.net.getId(),
234 ]);
236 if (!mounted) {
237 return;
238 }
240 setNonce(nonce);
241 setChainId(chainId);
242 } catch (error) {
243 setError(error);
244 }
245 })();
247 return () => (mounted = false);
248 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
250 useEffect(() => {
251 let mounted = true;
253 if (confirmed) {
254 (async () => {
255 if (refetch) {
256 try {
257 await refetch();
258 } catch (error) {
259 // log the original
260 console.error(error);
261 // track a user-friendly error
262 setError(
263 new Error(
264 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
265 )
266 );
267 }
268 }
270 if (!mounted) {
271 return;
272 }
274 setConfirmationProgress(1.0);
276 await timeout(PROGRESS_ANIMATION_DELAY_MS);
278 if (!mounted) {
279 return;
280 }
282 setState(STATE.COMPLETED);
283 })();
284 }
286 return () => (mounted = false);
287 }, [confirmed, refetch, completed, setError]);
289 const values = useDeepEqualReference({
290 isDefaultState,
291 initializing,
292 construct,
293 unconstruct,
294 constructed,
295 canSign,
296 generateAndSign,
297 signed,
298 broadcast,
299 broadcasted,
300 confirmed,
301 completed,
302 reset,
303 error,
304 inputsLocked,
305 confirmationProgress,
306 txHashes,
307 signedTransactions,
308 gasPrice,
309 setGasPrice,
310 resetGasPrice,
311 nonce,
312 chainId,
313 needFunds,
314 });
316 return { ...values, bind: values };