on 766cfe4 help sign in
bridge/src/lib/useEthereumTransaction.js
274 lines 7.0 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 setGasPrice(suggestedGasPrice);
179 setState(STATE.NONE);
180 setError(undefined);
181 setNeedFunds(undefined);
182 }, [suggestedGasPrice, setError]);
184 const resetGasPrice = useCallback(() => setGasPrice(suggestedGasPrice), [
185 setGasPrice,
186 suggestedGasPrice,
187 ]);
189 useEffect(() => {
190 // if nonce or chainId is undefined, re-fetch on-chain info
191 if (!(nonce === undefined || chainId === undefined)) {
192 return;
193 }
195 (async () => {
196 try {
197 setError(undefined);
198 const [nonce, chainId, estimatedGasPrice] = await Promise.all([
199 _web3.eth.getTransactionCount(_wallet.address),
200 _web3.eth.net.getId(),
201 _web3.eth.getGasPrice(),
202 ]);
204 setNonce(nonce);
205 setChainId(chainId);
206 const gasPrice = parseInt(fromWei(estimatedGasPrice, 'gwei'), 10);
207 setSuggestedGasPrice(gasPrice);
208 setGasPrice(gasPrice);
209 } catch (error) {
210 setError(error);
211 }
212 })();
213 }, [_wallet, _web3, setError, nonce, chainId]);
215 useEffect(() => {
216 let cancelled = false;
218 if (confirmed) {
219 (async () => {
220 if (refetch) {
221 try {
222 await refetch();
223 } catch (error) {
224 // log the original
225 console.error(error);
226 // track a user-friendly error
227 setError(
228 new Error(
229 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
230 )
231 );
232 }
233 }
235 if (cancelled) {
236 return;
237 }
239 setState(STATE.COMPLETED);
240 })();
241 }
243 return () => (cancelled = true);
244 }, [confirmed, refetch, completed, setError]);
246 const values = useDeepEqualReference({
247 isDefaultState,
248 initializing,
249 construct,
250 unconstruct,
251 constructed,
252 canSign,
253 generateAndSign,
254 signed,
255 broadcast,
256 broadcasted,
257 confirmed,
258 completed,
259 reset,
260 error,
261 inputsLocked,
262 //
263 txHash,
264 signedTransaction,
265 gasPrice,
266 setGasPrice,
267 resetGasPrice,
268 nonce,
269 chainId,
270 needFunds,
271 });
273 return { ...values, bind: values };