on c53aba4 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 { 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 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 };