on 0415772 help sign in
bridge/src/lib/useEthereumTransaction.js
270 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 (...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 // log the original
221 console.error(error);
222 // track a user-friendly error
223 setError(
224 new Error(
225 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
226 )
227 );
228 }
229 }
231 if (cancelled) {
232 return;
233 }
235 setState(STATE.COMPLETED);
236 })();
237 }
239 return () => (cancelled = true);
240 }, [confirmed, refetch, completed, setError]);
242 const values = useDeepEqualReference({
243 isDefaultState,
244 initializing,
245 construct,
246 unconstruct,
247 constructed,
248 canSign,
249 generateAndSign,
250 signed,
251 broadcast,
252 broadcasted,
253 confirmed,
254 completed,
255 reset,
256 error,
257 inputsLocked,
258 //
259 txHash,
260 signedTransaction,
261 gasPrice,
262 setGasPrice,
263 resetGasPrice,
264 nonce,
265 chainId,
266 needFunds,
267 });
269 return { ...values, bind: values };