on 6941c77 help sign in
bridge/src/lib/useEthereumTransaction.js
318 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 walletType,
208 ]);
210 const reset = useCallback(() => {
211 setTxHashes(undefined);
212 setSignedTransactions(undefined);
213 setNonce(undefined);
214 setChainId(undefined);
215 resetGasPrice();
216 setState(STATE.NONE);
217 setError(undefined);
218 setNeedFunds(undefined);
219 setConfirmationProgress(0.0);
220 }, [resetGasPrice, setError]);
222 useEffect(() => {
223 let mounted = true;
224 // if nonce or chainId is undefined, re-fetch on-chain info
225 if (!(nonce === undefined || chainId === undefined)) {
226 return;
227 }
229 (async () => {
230 try {
231 setError(undefined);
232 const [nonce, chainId] = await Promise.all([
233 _web3.eth.getTransactionCount(_wallet.address),
234 _web3.eth.net.getId(),
235 ]);
237 if (!mounted) {
238 return;
239 }
241 setNonce(nonce);
242 setChainId(chainId);
243 } catch (error) {
244 setError(error);
245 }
246 })();
248 return () => (mounted = false);
249 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
251 useEffect(() => {
252 let mounted = true;
254 if (confirmed) {
255 (async () => {
256 if (refetch) {
257 try {
258 await refetch();
259 } catch (error) {
260 // log the original
261 console.error(error);
262 // track a user-friendly error
263 setError(
264 new Error(
265 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
266 )
267 );
268 }
269 }
271 if (!mounted) {
272 return;
273 }
275 setConfirmationProgress(1.0);
277 await timeout(PROGRESS_ANIMATION_DELAY_MS);
279 if (!mounted) {
280 return;
281 }
283 setState(STATE.COMPLETED);
284 })();
285 }
287 return () => (mounted = false);
288 }, [confirmed, refetch, completed, setError]);
290 const values = useDeepEqualReference({
291 isDefaultState,
292 initializing,
293 construct,
294 unconstruct,
295 constructed,
296 canSign,
297 generateAndSign,
298 signed,
299 broadcast,
300 broadcasted,
301 confirmed,
302 completed,
303 reset,
304 error,
305 inputsLocked,
306 confirmationProgress,
307 txHashes,
308 signedTransactions,
309 gasPrice,
310 setGasPrice,
311 resetGasPrice,
312 nonce,
313 chainId,
314 needFunds,
315 });
317 return { ...values, bind: values };