on f4cb9dd help sign in
bridge/src/lib/useEthereumTransaction.js
315 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 cost,
161 rawTxs,
162 (address, minBalance, balance) =>
163 setNeedFunds({ address, minBalance, balance }),
164 () => setNeedFunds(undefined)
165 );
166 }
168 const txHashes = await signedTransactions.reduce(
169 (acc, stx) =>
170 acc.then(hashes =>
171 sendSignedTransaction(
172 _web3,
173 stx,
174 /* doubtNonceError */ usedTank
175 ).then(hash => [...hashes, hash])
176 ),
177 Promise.resolve([])
178 );
180 setTxHashes(txHashes);
181 setState(STATE.BROADCASTED);
183 await timeout(PROGRESS_ANIMATION_DELAY_MS);
185 setConfirmationProgress(0.2);
187 await Promise.all(
188 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
189 );
191 setConfirmationProgress(0.9);
193 setState(STATE.CONFIRMED);
194 } catch (error) {
195 setError(error);
196 }
197 }, [
198 _wallet.address,
199 _web3,
200 gasLimit,
201 gasPrice,
202 pointCursor,
203 setError,
204 signedTransactions,
205 ]);
207 const reset = useCallback(() => {
208 setTxHashes(undefined);
209 setSignedTransactions(undefined);
210 setNonce(undefined);
211 setChainId(undefined);
212 resetGasPrice();
213 setState(STATE.NONE);
214 setError(undefined);
215 setNeedFunds(undefined);
216 setConfirmationProgress(0.0);
217 }, [resetGasPrice, setError]);
219 useEffect(() => {
220 let mounted = true;
221 // if nonce or chainId is undefined, re-fetch on-chain info
222 if (!(nonce === undefined || chainId === undefined)) {
223 return;
224 }
226 (async () => {
227 try {
228 setError(undefined);
229 const [nonce, chainId] = await Promise.all([
230 _web3.eth.getTransactionCount(_wallet.address),
231 _web3.eth.net.getId(),
232 ]);
234 if (!mounted) {
235 return;
236 }
238 setNonce(nonce);
239 setChainId(chainId);
240 } catch (error) {
241 setError(error);
242 }
243 })();
245 return () => (mounted = false);
246 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
248 useEffect(() => {
249 let mounted = true;
251 if (confirmed) {
252 (async () => {
253 if (refetch) {
254 try {
255 await refetch();
256 } catch (error) {
257 // log the original
258 console.error(error);
259 // track a user-friendly error
260 setError(
261 new Error(
262 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
263 )
264 );
265 }
266 }
268 if (!mounted) {
269 return;
270 }
272 setConfirmationProgress(1.0);
274 await timeout(PROGRESS_ANIMATION_DELAY_MS);
276 if (!mounted) {
277 return;
278 }
280 setState(STATE.COMPLETED);
281 })();
282 }
284 return () => (mounted = false);
285 }, [confirmed, refetch, completed, setError]);
287 const values = useDeepEqualReference({
288 isDefaultState,
289 initializing,
290 construct,
291 unconstruct,
292 constructed,
293 canSign,
294 generateAndSign,
295 signed,
296 broadcast,
297 broadcasted,
298 confirmed,
299 completed,
300 reset,
301 error,
302 inputsLocked,
303 confirmationProgress,
304 txHashes,
305 signedTransactions,
306 gasPrice,
307 setGasPrice,
308 resetGasPrice,
309 nonce,
310 chainId,
311 needFunds,
312 });
314 return { ...values, bind: values };