on e1c3127 help sign in
bridge/src/lib/useEthereumTransaction.js
264 lines 6.7 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 (...args) => setUnsignedTransaction(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 setUnsignedTransaction(undefined);
177 setTxHash(undefined);
178 setSignedTransaction(undefined);
179 setGasPrice(suggestedGasPrice);
180 setState(STATE.NONE);
181 setError(undefined);
182 setNeedFunds(undefined);
183 }, [suggestedGasPrice, setError]);
185 const resetGasPrice = useCallback(() => setGasPrice(suggestedGasPrice), [
186 setGasPrice,
187 suggestedGasPrice,
188 ]);
190 useLifecycle(() => {
191 (async () => {
192 try {
193 setError(undefined);
194 const [nonce, chainId, estimatedGasPrice] = await Promise.all([
195 _web3.eth.getTransactionCount(_wallet.address),
196 _web3.eth.net.getId(),
197 _web3.eth.getGasPrice(),
198 ]);
200 setNonce(nonce);
201 setChainId(chainId);
202 const gasPrice = parseInt(fromWei(estimatedGasPrice, 'gwei'), 10);
203 setSuggestedGasPrice(gasPrice);
204 setGasPrice(gasPrice);
205 } catch (error) {
206 setError(error);
207 }
208 })();
209 });
211 useEffect(() => {
212 let cancelled = false;
214 if (confirmed) {
215 (async () => {
216 if (refetch) {
217 try {
218 await refetch();
219 } catch (error) {
220 // NOTE: we don't handle this error on purpose
221 console.error(error);
222 }
223 }
225 if (cancelled) {
226 return;
227 }
229 setState(STATE.COMPLETED);
230 })();
231 }
233 return () => (cancelled = true);
234 }, [confirmed, refetch, completed]);
236 const values = useDeepEqualReference({
237 isDefaultState,
238 initializing,
239 construct,
240 unconstruct,
241 constructed,
242 canSign,
243 generateAndSign,
244 signed,
245 broadcast,
246 broadcasted,
247 confirmed,
248 completed,
249 reset,
250 error,
251 inputsLocked,
252 //
253 txHash,
254 signedTransaction,
255 gasPrice,
256 setGasPrice,
257 resetGasPrice,
258 nonce,
259 chainId,
260 needFunds,
261 });
263 return { ...values, bind: values };