on a6f399f help sign in
bridge/src/lib/useEthereumTransaction.js
296 lines 7.4 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 [unsignedTransaction, setUnsignedTransaction] = useState();
70 const [signedTransaction, setSignedTransaction] = useState();
71 const [txHash, setTxHash] = useState();
72 const [needFunds, setNeedFunds] = useState();
73 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
75 const initializing = nonce === undefined || chainId === undefined;
76 const constructed = !!unsignedTransaction;
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 setUnsignedTransaction(await transactionBuilder(...args)),
92 [transactionBuilder]
93 );
95 const unconstruct = useCallback(() => setUnsignedTransaction(undefined), [
96 setUnsignedTransaction,
97 ]);
99 const generateAndSign = useCallback(async () => {
100 try {
101 setError(undefined);
103 const txn = await signTransaction({
104 wallet: _wallet,
105 walletType,
106 walletHdPath,
107 networkType,
108 txn: unsignedTransaction,
109 nonce,
110 chainId,
111 gasPrice: gasPrice.toFixed(0),
112 gasLimit,
113 });
115 setSignedTransaction(txn);
116 setState(STATE.SIGNED);
117 } catch (error) {
118 setError(error);
119 }
120 }, [
121 _wallet,
122 chainId,
123 gasLimit,
124 gasPrice,
125 networkType,
126 nonce,
127 setError,
128 unsignedTransaction,
129 walletHdPath,
130 walletType,
131 ]);
133 const broadcast = useCallback(async () => {
134 try {
135 setConfirmationProgress(0.0);
136 setError(undefined);
138 const rawTx = hexify(signedTransaction.serialize());
139 const cost = toWei(toBN(gasLimit).mul(toBN(gasPrice)), 'gwei');
141 let usedTank = false;
142 // if this ethereum transaction is being executed by a specific point
143 // see if we can use the tank
144 if (Just.hasInstance(pointCursor)) {
145 usedTank = await ensureFundsFor(
146 _web3,
147 pointCursor.value,
148 _wallet.address,
149 cost,
150 [rawTx],
151 (address, minBalance, balance) =>
152 setNeedFunds({ address, minBalance, balance }),
153 () => setNeedFunds(undefined)
154 );
155 }
157 const txHash = await sendSignedTransaction(
158 _web3,
159 signedTransaction,
160 /* doubtNonceError */ usedTank
161 );
163 setState(STATE.BROADCASTED);
164 setTxHash(txHash);
166 await timeout(PROGRESS_ANIMATION_DELAY_MS);
168 setConfirmationProgress(0.2);
170 await waitForTransactionConfirm(_web3, txHash);
172 setConfirmationProgress(0.9);
174 setState(STATE.CONFIRMED);
175 } catch (error) {
176 setError(error);
177 }
178 }, [
179 _wallet.address,
180 _web3,
181 gasLimit,
182 gasPrice,
183 pointCursor,
184 setError,
185 signedTransaction,
186 ]);
188 const reset = useCallback(() => {
189 setTxHash(undefined);
190 setSignedTransaction(undefined);
191 setNonce(undefined);
192 setChainId(undefined);
193 resetGasPrice();
194 setState(STATE.NONE);
195 setError(undefined);
196 setNeedFunds(undefined);
197 setConfirmationProgress(0.0);
198 }, [resetGasPrice, setError]);
200 useEffect(() => {
201 let mounted = true;
202 // if nonce or chainId is undefined, re-fetch on-chain info
203 if (!(nonce === undefined || chainId === undefined)) {
204 return;
205 }
207 (async () => {
208 try {
209 setError(undefined);
210 const [nonce, chainId] = await Promise.all([
211 _web3.eth.getTransactionCount(_wallet.address),
212 _web3.eth.net.getId(),
213 ]);
215 if (!mounted) {
216 return;
217 }
219 setNonce(nonce);
220 setChainId(chainId);
221 } catch (error) {
222 setError(error);
223 }
224 })();
226 return () => (mounted = false);
227 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
229 useEffect(() => {
230 let mounted = true;
232 if (confirmed) {
233 (async () => {
234 if (refetch) {
235 try {
236 await refetch();
237 } catch (error) {
238 // log the original
239 console.error(error);
240 // track a user-friendly error
241 setError(
242 new Error(
243 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
244 )
245 );
246 }
247 }
249 if (!mounted) {
250 return;
251 }
253 setConfirmationProgress(1.0);
255 await timeout(PROGRESS_ANIMATION_DELAY_MS);
257 if (!mounted) {
258 return;
259 }
261 setState(STATE.COMPLETED);
262 })();
263 }
265 return () => (mounted = false);
266 }, [confirmed, refetch, completed, setError]);
268 const values = useDeepEqualReference({
269 isDefaultState,
270 initializing,
271 construct,
272 unconstruct,
273 constructed,
274 canSign,
275 generateAndSign,
276 signed,
277 broadcast,
278 broadcasted,
279 confirmed,
280 completed,
281 reset,
282 error,
283 inputsLocked,
284 confirmationProgress,
285 txHash,
286 signedTransaction,
287 gasPrice,
288 setGasPrice,
289 resetGasPrice,
290 nonce,
291 chainId,
292 needFunds,
293 });
295 return { ...values, bind: values };