on 0ab8d9d help sign in
bridge/src/lib/useEthereumTransaction.js
274 lines 6.9 kB history raw
1import { useCallback, useEffect, useState } from 'react';
2import { Just } from 'folktale/maybe';
3import { toWei } from 'web3-utils';
5import { useNetwork } from 'store/network';
6import { useWallet } from 'store/wallet';
7import { usePointCursor } from 'store/pointCursor';
9import { GAS_LIMITS, DEFAULT_GAS_PRICE_GWEI } from './constants';
10import {
11 signTransaction,
12 sendSignedTransaction,
13 waitForTransactionConfirm,
14 hexify,
15} from './txn';
16import * as need from 'lib/need';
17import { ensureFundsFor } from 'lib/tank';
18import useDeepEqualReference from './useDeepEqualReference';
19import useGasPrice from './useGasPrice';
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 = DEFAULT_GAS_PRICE_GWEI
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, resetGasPrice } = useGasPrice(initialGasPrice);
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 setNonce(undefined);
179 setChainId(undefined);
180 resetGasPrice();
181 setState(STATE.NONE);
182 setError(undefined);
183 setNeedFunds(undefined);
184 }, [resetGasPrice, setError]);
186 useEffect(() => {
187 let mounted = true;
188 // if nonce or chainId is undefined, re-fetch on-chain info
189 if (!(nonce === undefined || chainId === undefined)) {
190 return;
191 }
193 (async () => {
194 try {
195 setError(undefined);
196 const [nonce, chainId] = await Promise.all([
197 _web3.eth.getTransactionCount(_wallet.address),
198 _web3.eth.net.getId(),
199 ]);
201 if (!mounted) {
202 return;
203 }
205 setNonce(nonce);
206 setChainId(chainId);
207 } catch (error) {
208 setError(error);
209 }
210 })();
212 return () => (mounted = false);
213 }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
215 useEffect(() => {
216 let mounted = true;
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 (!mounted) {
236 return;
237 }
239 setState(STATE.COMPLETED);
240 })();
241 }
243 return () => (mounted = false);
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 };