on 6526dd7 help sign in
bridge/src/lib/useEthereumTransaction.js
284 lines 7.2 kB history raw
1import { useCallback, useEffect, useState } from 'react';
2import { Just } from 'folktale/maybe';
3import { fromWei, toWei } from 'web3-utils';
5import { useNetwork } from 'store/network';
6import { useWallet } from 'store/wallet';
7import { usePointCursor } from 'store/pointCursor';
9import { GAS_LIMITS } from './constants';
10import {
11 signTransaction,
12 sendSignedTransaction,
13 waitForTransactionConfirm,
14 hexify,
15} from './txn';
16import * as need from './need';
17import { ensureFundsFor } from './tank';
18import useDeepEqualReference from './useDeepEqualReference';
20const STATE = {
21 NONE: 'NONE',
22 SIGNED: 'SIGNED',
23 BROADCASTED: 'BROADCASTED',
24 CONFIRMED: 'CONFIRMED',
25 COMPLETED: 'COMPLETED',
26};
28/**
29 * manage the state around sending and confirming an ethereum transaction
30 * @param {Transaction Function()} transactionBuilder
31 * @param {Promise<any> Function()} refetch async function called after completion
32 * @param {number} initialGasLimit gas limit
33 * @param {number} initialGasPrice gas price in gwei
34 */
35export default function useEthereumTransaction(
36 transactionBuilder,
37 refetch,
38 initialGasLimit = GAS_LIMITS.DEFAULT,
39 initialGasPrice = 20
40) {
41 const { wallet, walletType, walletHdPath } = useWallet();
42 const { web3, networkType } = useNetwork();
43 const { pointCursor } = usePointCursor();
45 const _web3 = need.web3(web3);
46 const _wallet = need.wallet(wallet);
48 const [error, _setError] = useState();
49 const setError = useCallback(
50 error => {
51 _setError(error);
52 if (error) {
53 console.error(error);
54 }
55 },
56 [_setError]
57 );
58 const [state, setState] = useState(STATE.NONE);
59 const [chainId, setChainId] = useState();
60 const [nonce, setNonce] = useState();
61 const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
62 const [suggestedGasPrice, setSuggestedGasPrice] = useState(gasPrice); // gwei
63 const [gasLimit] = useState(initialGasLimit);
64 const [unsignedTransaction, setUnsignedTransaction] = useState();
65 const [signedTransaction, setSignedTransaction] = useState();
66 const [txHash, setTxHash] = useState();
67 const [needFunds, setNeedFunds] = useState();
69 const initializing = nonce === undefined || chainId === undefined;
70 const constructed = !!unsignedTransaction;
71 const isDefaultState = state === STATE.NONE;
72 const signed = state === STATE.SIGNED;
73 const broadcasted = state === STATE.BROADCASTED;
74 const confirmed = state === STATE.CONFIRMED;
75 const completed = state === STATE.COMPLETED;
77 // lock inputs once we're out of the default state
78 const inputsLocked = !isDefaultState;
80 // we can sign when:
81 const canSign = !initializing && constructed && isDefaultState;
83 const construct = useCallback(
84 async (...args) =>
85 setUnsignedTransaction(await transactionBuilder(...args)),
86 [transactionBuilder]
87 );
89 const unconstruct = useCallback(() => setUnsignedTransaction(undefined), [
90 setUnsignedTransaction,
91 ]);
93 const generateAndSign = useCallback(async () => {
94 try {
95 setError(undefined);
97 const txn = await signTransaction({
98 wallet: _wallet,
99 walletType,
100 walletHdPath,
101 networkType,
102 txn: unsignedTransaction,
103 nonce,
104 chainId,
105 gasPrice: gasPrice.toFixed(0),
106 gasLimit,
107 });
109 setSignedTransaction(txn);
110 setState(STATE.SIGNED);
111 } catch (error) {
112 setError(error);
113 }
114 }, [
115 _wallet,
116 chainId,
117 gasLimit,
118 gasPrice,
119 networkType,
120 nonce,
121 setError,
122 unsignedTransaction,
123 walletHdPath,
124 walletType,
125 ]);
127 const broadcast = useCallback(async () => {
128 try {
129 setError(undefined);
131 const rawTx = hexify(signedTransaction.serialize());
132 const cost = toWei(gasPrice.toFixed(0), 'gwei') * gasLimit;
134 let usedTank = false;
135 // if this ethereum transaction is being executed by a specific point
136 // see if we can use the tank
137 if (Just.hasInstance(pointCursor)) {
138 usedTank = await ensureFundsFor(
139 _web3,
140 pointCursor.value,
141 _wallet.address,
142 cost,
143 [rawTx],
144 (address, minBalance, balance) =>
145 setNeedFunds({ address, minBalance, balance }),
146 () => setNeedFunds(undefined)
147 );
148 }
150 const txHash = await sendSignedTransaction(
151 _web3,
152 signedTransaction,
153 /* doubtNonceError */ usedTank
154 );
156 setState(STATE.BROADCASTED);
157 setTxHash(txHash);
159 await waitForTransactionConfirm(_web3, txHash);
161 setState(STATE.CONFIRMED);
162 } catch (error) {
163 setError(error);
164 }
165 }, [
166 _wallet.address,
167 _web3,
168 gasLimit,
169 gasPrice,
170 pointCursor,
171 setError,
172 signedTransaction,
173 ]);
175 const reset = useCallback(() => {
176 setTxHash(undefined);
177 setSignedTransaction(undefined);
178 setNonce(undefined);
179 setChainId(undefined);
180 setGasPrice(suggestedGasPrice);
181 setState(STATE.NONE);
182 setError(undefined);
183 setNeedFunds(undefined);
184 }, [suggestedGasPrice, setError]);
186 const resetGasPrice = useCallback(() => setGasPrice(suggestedGasPrice), [
187 setGasPrice,
188 suggestedGasPrice,
189 ]);
191 useEffect(() => {
192 let mounted = true;
193 // if nonce or chainId is undefined, re-fetch on-chain info
194 if (!(nonce === undefined || chainId === undefined)) {
195 return;
196 }
198 (async () => {
199 try {
200 setError(undefined);
201 const [nonce, chainId, estimatedGasPrice] = await Promise.all([
202 _web3.eth.getTransactionCount(_wallet.address),
203 _web3.eth.net.getId(),
204 _web3.eth.getGasPrice(),
205 ]);
207 const gasPrice = parseInt(fromWei(estimatedGasPrice, 'gwei'), 10);
209 if (!mounted) {
210 return;
211 }
213 setNonce(nonce);
214 setChainId(chainId);
215 setSuggestedGasPrice(gasPrice);
216 setGasPrice(gasPrice);
217 } catch (error) {
218 setError(error);
219 }
220 })();
222 return () => (mounted = false);
223 }, [_wallet, _web3, setError, nonce, chainId]);
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 setState(STATE.COMPLETED);
250 })();
251 }
253 return () => (mounted = false);
254 }, [confirmed, refetch, completed, setError]);
256 const values = useDeepEqualReference({
257 isDefaultState,
258 initializing,
259 construct,
260 unconstruct,
261 constructed,
262 canSign,
263 generateAndSign,
264 signed,
265 broadcast,
266 broadcasted,
267 confirmed,
268 completed,
269 reset,
270 error,
271 inputsLocked,
272 //
273 txHash,
274 signedTransaction,
275 gasPrice,
276 setGasPrice,
277 resetGasPrice,
278 nonce,
279 chainId,
280 needFunds,
281 });
283 return { ...values, bind: values };