on 8057f71 help sign in
bridge/src/lib/useEthereumTransaction.js
271 lines 6.9 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 useLifecycle from './useLifecycle';
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 [gasLimit] = useState(initialGasLimit);
65 const [unsignedTransaction, setUnsignedTransaction] = useState();
66 const [signedTransaction, setSignedTransaction] = useState();
67 const [txHash, setTxHash] = useState();
68 const [needFunds, setNeedFunds] = useState();
70 const initializing = nonce === undefined || chainId === undefined;
71 const constructed = !!unsignedTransaction;
72 const isDefaultState = state === STATE.NONE;
73 const signed = state === STATE.SIGNED;
74 const broadcasted = state === STATE.BROADCASTED;
75 const confirmed = state === STATE.CONFIRMED;
76 const completed = state === STATE.COMPLETED;
78 // lock inputs once we're out of the default state
79 const inputsLocked = !isDefaultState;
81 // we can sign when:
82 const canSign = !initializing && constructed && isDefaultState;
84 const construct = useCallback(
85 async (...args) =>
86 setUnsignedTransaction(await transactionBuilder(...args)),
87 [transactionBuilder]
88 );
90 const unconstruct = useCallback(() => setUnsignedTransaction(undefined), [
91 setUnsignedTransaction,
92 ]);
94 const generateAndSign = useCallback(async () => {
95 try {
96 setError(undefined);
98 const txn = await signTransaction({
99 wallet: _wallet,
100 walletType,
101 walletHdPath,
102 networkType,
103 txn: unsignedTransaction,
104 nonce,
105 chainId,
106 gasPrice: gasPrice.toFixed(0),
107 gasLimit,
108 });
110 setSignedTransaction(txn);
111 setState(STATE.SIGNED);
112 } catch (error) {
113 setError(error);
114 }
115 }, [
116 _wallet,
117 chainId,
118 gasLimit,
119 gasPrice,
120 networkType,
121 nonce,
122 setError,
123 unsignedTransaction,
124 walletHdPath,
125 walletType,
126 ]);
128 const broadcast = useCallback(async () => {
129 try {
130 setError(undefined);
132 const rawTx = hexify(signedTransaction.serialize());
133 const cost = toWei(gasPrice.toFixed(0), 'gwei') * gasLimit;
135 let usedTank = false;
136 // if this ethereum transaction is being executed by a specific point
137 // see if we can use the tank
138 if (Just.hasInstance(pointCursor)) {
139 usedTank = await ensureFundsFor(
140 _web3,
141 pointCursor.value,
142 _wallet.address,
143 cost,
144 [rawTx],
145 (address, minBalance, balance) =>
146 setNeedFunds({ address, minBalance, balance }),
147 () => setNeedFunds(undefined)
148 );
149 }
151 const txHash = await sendSignedTransaction(
152 _web3,
153 signedTransaction,
154 /* doubtNonceError */ usedTank
155 );
157 setState(STATE.BROADCASTED);
158 setTxHash(txHash);
160 await waitForTransactionConfirm(_web3, txHash);
162 setState(STATE.CONFIRMED);
163 } catch (error) {
164 setError(error);
165 }
166 }, [
167 _wallet.address,
168 _web3,
169 gasLimit,
170 gasPrice,
171 pointCursor,
172 setError,
173 signedTransaction,
174 ]);
176 const reset = useCallback(() => {
177 setUnsignedTransaction(undefined);
178 setTxHash(undefined);
179 setSignedTransaction(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 useLifecycle(() => {
192 (async () => {
193 try {
194 setError(undefined);
195 const [nonce, chainId, estimatedGasPrice] = await Promise.all([
196 _web3.eth.getTransactionCount(_wallet.address),
197 _web3.eth.net.getId(),
198 _web3.eth.getGasPrice(),
199 ]);
201 setNonce(nonce);
202 setChainId(chainId);
203 const gasPrice = parseInt(fromWei(estimatedGasPrice, 'gwei'), 10);
204 setSuggestedGasPrice(gasPrice);
205 setGasPrice(gasPrice);
206 } catch (error) {
207 setError(error);
208 }
209 })();
210 });
212 useEffect(() => {
213 let cancelled = false;
215 if (confirmed) {
216 (async () => {
217 if (refetch) {
218 try {
219 await refetch();
220 } catch (error) {
221 // log the original
222 console.error(error);
223 // track a user-friendly error
224 setError(
225 new Error(
226 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
227 )
228 );
229 }
230 }
232 if (cancelled) {
233 return;
234 }
236 setState(STATE.COMPLETED);
237 })();
238 }
240 return () => (cancelled = true);
241 }, [confirmed, refetch, completed, setError]);
243 const values = useDeepEqualReference({
244 isDefaultState,
245 initializing,
246 construct,
247 unconstruct,
248 constructed,
249 canSign,
250 generateAndSign,
251 signed,
252 broadcast,
253 broadcasted,
254 confirmed,
255 completed,
256 reset,
257 error,
258 inputsLocked,
259 //
260 txHash,
261 signedTransaction,
262 gasPrice,
263 setGasPrice,
264 resetGasPrice,
265 nonce,
266 chainId,
267 needFunds,
268 });
270 return { ...values, bind: values };