on 54fae1c help sign in
bridge/src/lib/useEthereumTransaction.js
292 lines 7.4 kB history raw
1import { useCallback, useEffect, useState } from 'react';
2import { Just } from 'folktale/maybe';
3import { toWei } from 'web3-utils';
5import { useNetwork } from 'store/network';
6import { useWallet } from 'store/wallet';
7import { usePointCursor } from 'store/pointCursor';
9import { GAS_LIMITS, DEFAULT_GAS_PRICE_GWEI } from './constants';
10import {
11 signTransaction,
12 sendSignedTransaction,
13 waitForTransactionConfirm,
14 hexify,
15} from './txn';
16import * as need from 'lib/need';
17import { ensureFundsFor } from 'lib/tank';
18import useDeepEqualReference from './useDeepEqualReference';
19import useGasPrice from './useGasPrice';
20import timeout from './timeout';
22const STATE = {
23 NONE: 'NONE',
24 SIGNED: 'SIGNED',
25 BROADCASTED: 'BROADCASTED',
26 CONFIRMED: 'CONFIRMED',
27 COMPLETED: 'COMPLETED',
28};
30/**
31 * manage the state around sending and confirming an ethereum transaction
32 * @param {Transaction Function()} transactionBuilder
33 * @param {Promise<any> Function()} refetch async function called after completion
34 * @param {number} initialGasLimit gas limit
35 * @param {number} initialGasPrice gas price in gwei
36 */
37export default function useEthereumTransaction(
38 transactionBuilder,
39 refetch,
40 initialGasLimit = GAS_LIMITS.DEFAULT,
41 initialGasPrice = DEFAULT_GAS_PRICE_GWEI
42) {
43 const { wallet, walletType, walletHdPath } = useWallet();
44 const { web3, networkType } = useNetwork();
45 const { pointCursor } = usePointCursor();
47 const _web3 = need.web3(web3);
48 const _wallet = need.wallet(wallet);
50 const [error, _setError] = useState();
51 const setError = useCallback(
52 error => {
53 _setError(error);
54 if (error) {
55 console.error(error);
56 }
57 },
58 [_setError]
59 );
60 const [state, setState] = useState(STATE.NONE);
61 const [chainId, setChainId] = useState();
62 const [nonce, setNonce] = useState();
63 const { gasPrice, setGasPrice, resetGasPrice } = useGasPrice(initialGasPrice);
64 const [gasLimit] = useState(initialGasLimit);
65 const [unsignedTransaction, setUnsignedTransaction] = useState();
66 const [signedTransaction, setSignedTransaction] = useState();
67 const [txHash, setTxHash] = useState();
68 const [needFunds, setNeedFunds] = useState();
69 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
71 const initializing = nonce === undefined || chainId === undefined;
72 const constructed = !!unsignedTransaction;
73 const isDefaultState = state === STATE.NONE;
74 const signed = state === STATE.SIGNED;
75 const broadcasted = state === STATE.BROADCASTED;
76 const confirmed = state === STATE.CONFIRMED;
77 const completed = state === STATE.COMPLETED;
79 // lock inputs once we're out of the default state
80 const inputsLocked = !isDefaultState;
82 // we can sign when:
83 const canSign = !initializing && constructed && isDefaultState;
85 const construct = useCallback(
86 async (...args) =>
87 setUnsignedTransaction(await transactionBuilder(...args)),
88 [transactionBuilder]
89 );
91 const unconstruct = useCallback(() => setUnsignedTransaction(undefined), [
92 setUnsignedTransaction,
93 ]);
95 const generateAndSign = useCallback(async () => {
96 try {
97 setError(undefined);
99 const txn = await signTransaction({
100 wallet: _wallet,
101 walletType,
102 walletHdPath,
103 networkType,
104 txn: unsignedTransaction,
105 nonce,
106 chainId,
107 gasPrice: gasPrice.toFixed(0),
108 gasLimit,
109 });
111 setSignedTransaction(txn);
112 setState(STATE.SIGNED);
113 } catch (error) {
114 setError(error);
115 }
116 }, [
117 _wallet,
118 chainId,
119 gasLimit,
120 gasPrice,
121 networkType,
122 nonce,
123 setError,
124 unsignedTransaction,
125 walletHdPath,
126 walletType,
127 ]);
129 const broadcast = useCallback(async () => {
130 try {
131 setConfirmationProgress(0.0);
132 setError(undefined);
134 const rawTx = hexify(signedTransaction.serialize());
135 const cost = toWei(gasPrice.toFixed(0), 'gwei') * gasLimit;
137 let usedTank = false;
138 // if this ethereum transaction is being executed by a specific point
139 // see if we can use the tank
140 if (Just.hasInstance(pointCursor)) {
141 usedTank = await ensureFundsFor(
142 _web3,
143 pointCursor.value,
144 _wallet.address,
145 cost,
146 [rawTx],
147 (address, minBalance, balance) =>
148 setNeedFunds({ address, minBalance, balance }),
149 () => setNeedFunds(undefined)
150 );
151 }
153 const txHash = await sendSignedTransaction(
154 _web3,
155 signedTransaction,
156 /* doubtNonceError */ usedTank
157 );
159 setState(STATE.BROADCASTED);
160 setTxHash(txHash);
162 await timeout(500); // wait for .animated-width to complete
164 setConfirmationProgress(0.2);
166 await waitForTransactionConfirm(_web3, txHash);
168 setConfirmationProgress(0.9);
170 setState(STATE.CONFIRMED);
171 } catch (error) {
172 setError(error);
173 }
174 }, [
175 _wallet.address,
176 _web3,
177 gasLimit,
178 gasPrice,
179 pointCursor,
180 setError,
181 signedTransaction,
182 ]);
184 const reset = useCallback(() => {
185 setTxHash(undefined);
186 setSignedTransaction(undefined);
187 setNonce(undefined);
188 setChainId(undefined);
189 resetGasPrice();
190 setState(STATE.NONE);
191 setError(undefined);
192 setNeedFunds(undefined);
193 setConfirmationProgress(0.0);
194 }, [resetGasPrice, setError]);
196 useEffect(() => {
197 let mounted = true;
198 // if nonce or chainId is undefined, re-fetch on-chain info
199 if (!(nonce === undefined || chainId === undefined)) {
200 return;
201 }
203 (async () => {
204 try {
205 setError(undefined);
206 const [nonce, chainId] = await Promise.all([
207 _web3.eth.getTransactionCount(_wallet.address),
208 _web3.eth.net.getId(),
209 ]);
211 if (!mounted) {
212 return;
213 }
215 setNonce(nonce);
216 setChainId(chainId);
217 } catch (error) {
218 setError(error);
219 }
220 })();
222 return () => (mounted = false);
223 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
225 useEffect(() => {
226 let mounted = true;
228 if (confirmed) {
229 (async () => {
230 if (refetch) {
231 try {
232 await refetch();
233 } catch (error) {
234 // log the original
235 console.error(error);
236 // track a user-friendly error
237 setError(
238 new Error(
239 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
240 )
241 );
242 }
243 }
245 if (!mounted) {
246 return;
247 }
249 setConfirmationProgress(1.0);
251 await timeout(500); // wait for .animated-width to complete
253 if (!mounted) {
254 return;
255 }
257 setState(STATE.COMPLETED);
258 })();
259 }
261 return () => (mounted = false);
262 }, [confirmed, refetch, completed, setError]);
264 const values = useDeepEqualReference({
265 isDefaultState,
266 initializing,
267 construct,
268 unconstruct,
269 constructed,
270 canSign,
271 generateAndSign,
272 signed,
273 broadcast,
274 broadcasted,
275 confirmed,
276 completed,
277 reset,
278 error,
279 inputsLocked,
280 confirmationProgress,
281 txHash,
282 signedTransaction,
283 gasPrice,
284 setGasPrice,
285 resetGasPrice,
286 nonce,
287 chainId,
288 needFunds,
289 });
291 return { ...values, bind: values };