on 4a434b6 help sign in
bridge/src/lib/useEthereumTransaction.js
283 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';
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 // 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 setNonce(nonce);
217 setChainId(chainId);
218 } catch (error) {
219 setError(error);
220 }
221 })();
222 }, [_wallet, _web3, setError, nonce, chainId, networkType]);
224 useEffect(() => {
225 let cancelled = false;
227 if (confirmed) {
228 (async () => {
229 if (refetch) {
230 try {
231 await refetch();
232 } catch (error) {
233 // log the original
234 console.error(error);
235 // track a user-friendly error
236 setError(
237 new Error(
238 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
239 )
240 );
241 }
242 }
244 if (cancelled) {
245 return;
246 }
248 setState(STATE.COMPLETED);
249 })();
250 }
252 return () => (cancelled = true);
253 }, [confirmed, refetch, completed, setError]);
255 const values = useDeepEqualReference({
256 isDefaultState,
257 initializing,
258 construct,
259 unconstruct,
260 constructed,
261 canSign,
262 generateAndSign,
263 signed,
264 broadcast,
265 broadcasted,
266 confirmed,
267 completed,
268 reset,
269 error,
270 inputsLocked,
271 //
272 txHash,
273 signedTransaction,
274 gasPrice,
275 setGasPrice,
276 resetGasPrice,
277 nonce,
278 chainId,
279 needFunds,
280 });
282 return { ...values, bind: values };