on 884e294 help sign in
bridge/src/lib/useEthereumTransaction.js
316 lines 8.0 kB history raw
1import { useCallback, useEffect, useState } from 'react';
2import { Just } from 'folktale/maybe';
3import { toWei, 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';
26const STATE = {
27 NONE: 'NONE',
28 SIGNED: 'SIGNED',
29 BROADCASTED: 'BROADCASTED',
30 CONFIRMED: 'CONFIRMED',
31 COMPLETED: 'COMPLETED',
32};
34/**
35 * manage the state around sending and confirming an ethereum transaction
36 * @param {Transaction Function()} transactionBuilder
37 * @param {Promise<any> Function()} refetch async function called after completion
38 * @param {number} initialGasLimit gas limit
39 * @param {number} initialGasPrice gas price in gwei
40 */
41export default function useEthereumTransaction(
42 transactionBuilder,
43 refetch,
44 initialGasLimit = GAS_LIMITS.DEFAULT,
45 initialGasPrice = DEFAULT_GAS_PRICE_GWEI
46) {
47 const { wallet, walletType, walletHdPath } = useWallet();
48 const { web3, networkType } = useNetwork();
49 const { pointCursor } = usePointCursor();
51 const _web3 = need.web3(web3);
52 const _wallet = need.wallet(wallet);
54 const [error, _setError] = useState();
55 const setError = useCallback(
56 error => {
57 _setError(error);
58 if (error) {
59 console.error(error);
60 }
61 },
62 [_setError]
63 );
64 const [state, setState] = useState(STATE.NONE);
65 const [chainId, setChainId] = useState();
66 const [nonce, setNonce] = useState();
67 const { gasPrice, setGasPrice, resetGasPrice } = useGasPrice(initialGasPrice);
68 const [gasLimit] = useState(initialGasLimit);
69 const [unsignedTransactions, setUnsignedTransactions] = useState();
70 const [signedTransactions, setSignedTransactions] = useState();
71 const [txHashes, setTxHashes] = useState();
72 const [needFunds, setNeedFunds] = useState();
73 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
75 const initializing = nonce === undefined || chainId === undefined;
76 const constructed = !!unsignedTransactions;
77 const isDefaultState = state === STATE.NONE;
78 const signed = state === STATE.SIGNED;
79 const broadcasted = state === STATE.BROADCASTED;
80 const confirmed = state === STATE.CONFIRMED;
81 const completed = state === STATE.COMPLETED;
83 // lock inputs once we're out of the default state
84 const inputsLocked = !isDefaultState;
86 // we can sign when:
87 const canSign = !initializing && constructed && isDefaultState;
89 const construct = useCallback(
90 async (...args) =>
91 setUnsignedTransactions(await transactionBuilder(...args)),
92 [transactionBuilder]
93 );
95 const unconstruct = useCallback(() => setUnsignedTransactions(undefined), [
96 setUnsignedTransactions,
97 ]);
99 const generateAndSign = useCallback(async () => {
100 try {
101 setError(undefined);
103 const utxs = Array.isArray(unsignedTransactions)
104 ? unsignedTransactions
105 : [unsignedTransactions];
107 const txns = await Promise.all(
108 utxs.map((utx, i) =>
109 signTransaction({
110 wallet: _wallet,
111 walletType,
112 walletHdPath,
113 networkType,
114 txn: utx,
115 nonce: nonce + i,
116 chainId,
117 gasPrice: gasPrice.toFixed(0),
118 gasLimit,
119 })
120 )
121 );
123 setSignedTransactions(txns);
124 setState(STATE.SIGNED);
125 } catch (error) {
126 setError(error);
127 }
128 }, [
129 _wallet,
130 chainId,
131 gasLimit,
132 gasPrice,
133 networkType,
134 nonce,
135 setError,
136 unsignedTransactions,
137 walletHdPath,
138 walletType,
139 ]);
141 const broadcast = useCallback(async () => {
142 try {
143 setConfirmationProgress(0.0);
144 setError(undefined);
146 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
148 const costGwei = toBN(gasLimit)
149 .mul(toBN(gasPrice))
150 .mul(toBN(rawTxs.length));
151 const cost = toWei(costGwei.toString(), 'gwei');
152 let usedTank = false;
153 // if this ethereum transaction is being executed by a specific point
154 // see if we can use the tank
155 if (Just.hasInstance(pointCursor)) {
156 usedTank = await ensureFundsFor(
157 _web3,
158 pointCursor.value,
159 _wallet.address,
160 walletType,
161 cost,
162 rawTxs,
163 (address, minBalance, balance) =>
164 setNeedFunds({ address, minBalance, balance }),
165 () => setNeedFunds(undefined)
166 );
167 }
169 const txHashes = await signedTransactions.reduce(
170 (acc, stx) =>
171 acc.then(hashes =>
172 sendSignedTransaction(
173 _web3,
174 stx,
175 /* doubtNonceError */ usedTank
176 ).then(hash => [...hashes, hash])
177 ),
178 Promise.resolve([])
179 );
181 setTxHashes(txHashes);
182 setState(STATE.BROADCASTED);
184 await timeout(PROGRESS_ANIMATION_DELAY_MS);
186 setConfirmationProgress(0.2);
188 await Promise.all(
189 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
190 );
192 setConfirmationProgress(0.9);
194 setState(STATE.CONFIRMED);
195 } catch (error) {
196 setError(error);
197 }
198 }, [
199 _wallet.address,
200 _web3,
201 gasLimit,
202 gasPrice,
203 pointCursor,
204 setError,
205 signedTransactions,
206 ]);
208 const reset = useCallback(() => {
209 setTxHashes(undefined);
210 setSignedTransactions(undefined);
211 setNonce(undefined);
212 setChainId(undefined);
213 resetGasPrice();
214 setState(STATE.NONE);
215 setError(undefined);
216 setNeedFunds(undefined);
217 setConfirmationProgress(0.0);
218 }, [resetGasPrice, setError]);
220 useEffect(() => {
221 let mounted = true;
222 // if nonce or chainId is undefined, re-fetch on-chain info
223 if (!(nonce === undefined || chainId === undefined)) {
224 return;
225 }
227 (async () => {
228 try {
229 setError(undefined);
230 const [nonce, chainId] = await Promise.all([
231 _web3.eth.getTransactionCount(_wallet.address),
232 _web3.eth.net.getId(),
233 ]);
235 if (!mounted) {
236 return;
237 }
239 setNonce(nonce);
240 setChainId(chainId);
241 } catch (error) {
242 setError(error);
243 }
244 })();
246 return () => (mounted = false);
247 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
249 useEffect(() => {
250 let mounted = true;
252 if (confirmed) {
253 (async () => {
254 if (refetch) {
255 try {
256 await refetch();
257 } catch (error) {
258 // log the original
259 console.error(error);
260 // track a user-friendly error
261 setError(
262 new Error(
263 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
264 )
265 );
266 }
267 }
269 if (!mounted) {
270 return;
271 }
273 setConfirmationProgress(1.0);
275 await timeout(PROGRESS_ANIMATION_DELAY_MS);
277 if (!mounted) {
278 return;
279 }
281 setState(STATE.COMPLETED);
282 })();
283 }
285 return () => (mounted = false);
286 }, [confirmed, refetch, completed, setError]);
288 const values = useDeepEqualReference({
289 isDefaultState,
290 initializing,
291 construct,
292 unconstruct,
293 constructed,
294 canSign,
295 generateAndSign,
296 signed,
297 broadcast,
298 broadcasted,
299 confirmed,
300 completed,
301 reset,
302 error,
303 inputsLocked,
304 confirmationProgress,
305 txHashes,
306 signedTransactions,
307 gasPrice,
308 setGasPrice,
309 resetGasPrice,
310 nonce,
311 chainId,
312 needFunds,
313 });
315 return { ...values, bind: values };