on 302442c help sign in
bridge/src/lib/useEthereumTransaction.js
298 lines 7.4 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';
8import { useSuggestedGasPrice } from 'lib/useSuggestedGasPrice';
10import { GAS_LIMITS } from './constants';
11import {
12 signTransaction,
13 sendSignedTransaction,
14 waitForTransactionConfirm,
15 hexify,
16} from './txn';
17import * as need from './need';
18import { ensureFundsFor } from './tank';
19import useDeepEqualReference from './useDeepEqualReference';
21const STATE = {
22 NONE: 'NONE',
23 SIGNED: 'SIGNED',
24 BROADCASTED: 'BROADCASTED',
25 CONFIRMED: 'CONFIRMED',
26 COMPLETED: 'COMPLETED',
27};
29/**
30 * manage the state around sending and confirming an ethereum transaction
31 * @param {Transaction Function()} transactionBuilder
32 * @param {Promise<any> Function()} refetch async function called after completion
33 * @param {number} initialGasLimit gas limit
34 * @param {number} initialGasPrice gas price in gwei
35 */
36export default function useEthereumTransaction(
37 transactionBuilder,
38 refetch,
39 initialGasLimit = GAS_LIMITS.DEFAULT,
40 initialGasPrice = 20
41) {
42 const { wallet, walletType, walletHdPath } = useWallet();
43 const { web3, networkType } = useNetwork();
44 const { pointCursor } = usePointCursor();
46 const _web3 = need.web3(web3);
47 const _wallet = need.wallet(wallet);
49 const [error, _setError] = useState();
50 const setError = useCallback(
51 error => {
52 _setError(error);
53 if (error) {
54 console.error(error);
55 }
56 },
57 [_setError]
58 );
59 const [state, setState] = useState(STATE.NONE);
60 const [chainId, setChainId] = useState();
61 const [nonce, setNonce] = useState();
62 const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
63 const [suggestedGasPrice, setSuggestedGasPrice] = useState(gasPrice); // gwei
64 const { gasPrice: estimatedGasPrice } = useSuggestedGasPrice();
65 const [gasLimit] = useState(initialGasLimit);
66 const [unsignedTransaction, setUnsignedTransaction] = useState();
67 const [signedTransaction, setSignedTransaction] = useState();
68 const [txHash, setTxHash] = useState();
69 const [needFunds, setNeedFunds] = useState();
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 setError(undefined);
133 const rawTx = hexify(signedTransaction.serialize());
134 const cost = toWei(gasPrice.toFixed(0), 'gwei') * gasLimit;
136 let usedTank = false;
137 // if this ethereum transaction is being executed by a specific point
138 // see if we can use the tank
139 if (Just.hasInstance(pointCursor)) {
140 usedTank = await ensureFundsFor(
141 _web3,
142 pointCursor.value,
143 _wallet.address,
144 cost,
145 [rawTx],
146 (address, minBalance, balance) =>
147 setNeedFunds({ address, minBalance, balance }),
148 () => setNeedFunds(undefined)
149 );
150 }
152 const txHash = await sendSignedTransaction(
153 _web3,
154 signedTransaction,
155 /* doubtNonceError */ usedTank
156 );
158 setState(STATE.BROADCASTED);
159 setTxHash(txHash);
161 await waitForTransactionConfirm(_web3, txHash);
163 setState(STATE.CONFIRMED);
164 } catch (error) {
165 setError(error);
166 }
167 }, [
168 _wallet.address,
169 _web3,
170 gasLimit,
171 gasPrice,
172 pointCursor,
173 setError,
174 signedTransaction,
175 ]);
177 const reset = useCallback(() => {
178 setTxHash(undefined);
179 setSignedTransaction(undefined);
180 setNonce(undefined);
181 setChainId(undefined);
182 setGasPrice(suggestedGasPrice);
183 setState(STATE.NONE);
184 setError(undefined);
185 setNeedFunds(undefined);
186 }, [suggestedGasPrice, setError]);
188 const resetGasPrice = useCallback(() => setGasPrice(suggestedGasPrice), [
189 setGasPrice,
190 suggestedGasPrice,
191 ]);
193 useEffect(() => {
194 const gasPrice = parseInt(
195 fromWei(estimatedGasPrice.toString(), 'gwei'),
196 10
197 );
198 setSuggestedGasPrice(gasPrice);
199 setGasPrice(gasPrice);
200 }, [estimatedGasPrice]);
202 useEffect(() => {
203 let mounted = true;
204 // if nonce or chainId is undefined, re-fetch on-chain info
205 if (!(nonce === undefined || chainId === undefined)) {
206 return;
207 }
209 (async () => {
210 try {
211 setError(undefined);
212 const [nonce, chainId] = await Promise.all([
213 _web3.eth.getTransactionCount(_wallet.address),
214 _web3.eth.net.getId(),
215 ]);
217 if (!mounted) {
218 return;
219 }
221 setNonce(nonce);
222 setChainId(chainId);
223 } catch (error) {
224 setError(error);
225 }
226 })();
228 return () => (mounted = false);
229 }, [
230 _wallet,
231 _web3,
232 setError,
233 nonce,
234 chainId,
235 networkType,
236 estimatedGasPrice,
237 ]);
239 useEffect(() => {
240 let mounted = true;
242 if (confirmed) {
243 (async () => {
244 if (refetch) {
245 try {
246 await refetch();
247 } catch (error) {
248 // log the original
249 console.error(error);
250 // track a user-friendly error
251 setError(
252 new Error(
253 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
254 )
255 );
256 }
257 }
259 if (!mounted) {
260 return;
261 }
263 setState(STATE.COMPLETED);
264 })();
265 }
267 return () => (mounted = false);
268 }, [confirmed, refetch, completed, setError]);
270 const values = useDeepEqualReference({
271 isDefaultState,
272 initializing,
273 construct,
274 unconstruct,
275 constructed,
276 canSign,
277 generateAndSign,
278 signed,
279 broadcast,
280 broadcasted,
281 confirmed,
282 completed,
283 reset,
284 error,
285 inputsLocked,
286 //
287 txHash,
288 signedTransaction,
289 gasPrice,
290 setGasPrice,
291 resetGasPrice,
292 nonce,
293 chainId,
294 needFunds,
295 });
297 return { ...values, bind: values };