on 97cbb00 help sign in
bridge/src/lib/useEthereumTransaction.js
297 lines 7.5 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 costGwei = toBN(gasLimit).mul(toBN(gasPrice));
140 const cost = toWei(costGwei.toString(), 'gwei');
142 let usedTank = false;
143 // if this ethereum transaction is being executed by a specific point
144 // see if we can use the tank
145 if (Just.hasInstance(pointCursor)) {
146 usedTank = await ensureFundsFor(
147 _web3,
148 pointCursor.value,
149 _wallet.address,
150 cost,
151 [rawTx],
152 (address, minBalance, balance) =>
153 setNeedFunds({ address, minBalance, balance }),
154 () => setNeedFunds(undefined)
155 );
156 }
158 const txHash = await sendSignedTransaction(
159 _web3,
160 signedTransaction,
161 /* doubtNonceError */ usedTank
162 );
164 setState(STATE.BROADCASTED);
165 setTxHash(txHash);
167 await timeout(PROGRESS_ANIMATION_DELAY_MS);
169 setConfirmationProgress(0.2);
171 await waitForTransactionConfirm(_web3, txHash);
173 setConfirmationProgress(0.9);
175 setState(STATE.CONFIRMED);
176 } catch (error) {
177 setError(error);
178 }
179 }, [
180 _wallet.address,
181 _web3,
182 gasLimit,
183 gasPrice,
184 pointCursor,
185 setError,
186 signedTransaction,
187 ]);
189 const reset = useCallback(() => {
190 setTxHash(undefined);
191 setSignedTransaction(undefined);
192 setNonce(undefined);
193 setChainId(undefined);
194 resetGasPrice();
195 setState(STATE.NONE);
196 setError(undefined);
197 setNeedFunds(undefined);
198 setConfirmationProgress(0.0);
199 }, [resetGasPrice, setError]);
201 useEffect(() => {
202 let mounted = true;
203 // if nonce or chainId is undefined, re-fetch on-chain info
204 if (!(nonce === undefined || chainId === undefined)) {
205 return;
206 }
208 (async () => {
209 try {
210 setError(undefined);
211 const [nonce, chainId] = await Promise.all([
212 _web3.eth.getTransactionCount(_wallet.address),
213 _web3.eth.net.getId(),
214 ]);
216 if (!mounted) {
217 return;
218 }
220 setNonce(nonce);
221 setChainId(chainId);
222 } catch (error) {
223 setError(error);
224 }
225 })();
227 return () => (mounted = false);
228 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
230 useEffect(() => {
231 let mounted = true;
233 if (confirmed) {
234 (async () => {
235 if (refetch) {
236 try {
237 await refetch();
238 } catch (error) {
239 // log the original
240 console.error(error);
241 // track a user-friendly error
242 setError(
243 new Error(
244 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
245 )
246 );
247 }
248 }
250 if (!mounted) {
251 return;
252 }
254 setConfirmationProgress(1.0);
256 await timeout(PROGRESS_ANIMATION_DELAY_MS);
258 if (!mounted) {
259 return;
260 }
262 setState(STATE.COMPLETED);
263 })();
264 }
266 return () => (mounted = false);
267 }, [confirmed, refetch, completed, setError]);
269 const values = useDeepEqualReference({
270 isDefaultState,
271 initializing,
272 construct,
273 unconstruct,
274 constructed,
275 canSign,
276 generateAndSign,
277 signed,
278 broadcast,
279 broadcasted,
280 confirmed,
281 completed,
282 reset,
283 error,
284 inputsLocked,
285 confirmationProgress,
286 txHash,
287 signedTransaction,
288 gasPrice,
289 setGasPrice,
290 resetGasPrice,
291 nonce,
292 chainId,
293 needFunds,
294 });
296 return { ...values, bind: values };