on c8cd811 help sign in
bridge/src/lib/txn.ts
271 lines 8.0 kB history raw
1import { Common, Chain, Hardfork } from '@ethereumjs/common';
2import {
3 FeeMarketEIP1559Transaction as EIP1559Transaction,
4 TxOptions,
5 FeeMarketEIP1559TxData as EIP1559TxData,
6} from '@ethereumjs/tx';
7import { toHex, toWei } from 'web3-utils';
8import retry from 'async-retry';
9import { NETWORK_TYPES } from './network';
10import { walletConnectSignTransaction } from './WalletConnect';
11import { metamaskSignTransaction } from './metamask';
12import { addHexPrefix } from './utils/address';
13import {
14 CHECK_BLOCK_EVERY_MS,
15 EIP1559_TRANSACTION_TYPE,
16 WALLET_TYPES,
17} from './constants';
18import Web3 from 'web3';
19import { TransactionConfig, TransactionReceipt } from 'web3-core';
20import BridgeWallet from './types/BridgeWallet';
21import { GasPriceData } from 'components/L2/Dropdowns/FeeDropdown';
22import { ITxData } from './types/ITxData';
24const RETRY_OPTIONS = {
25 retries: 99999,
26 factor: 1,
27 minTimeout: CHECK_BLOCK_EVERY_MS,
28 randomize: false,
29};
31export type FakeSignableTx = EIP1559Transaction;
33type TxSender = (txn: FakeSignableTx, web3: Web3) => Promise<string>;
35export type FakeSignedTx = {
36 txn: FakeSignableTx;
37 send: TxSender;
38 serialize: () => string;
39};
41const FakeSignResult = (txn: FakeSignableTx, send: TxSender): FakeSignedTx => {
42 return {
43 txn,
44 serialize: () => {
45 return '???'; //NOTE update tank.js when changing this!
46 },
47 send,
48 };
49};
51interface signTransactionProps {
52 wallet: BridgeWallet;
53 walletType: symbol;
54 walletHdPath: string; // TODO: is this still needed?
55 networkType: symbol;
56 txn: { data: string; to: string; value: number };
57 nonce: number | string;
58 chainId: number | string;
59 gasPriceData: GasPriceData;
60 gasLimit: number;
61 txnSigner?: (args: ITxData) => Promise<string>;
62 txnSender?: (args: ITxData) => Promise<string>;
65const _web3 = () => {
66 const ENDPOINT = `https://mainnet.infura.io/v3/${
67 import.meta.env.VITE_INFURA_ENDPOINT
68 }`;
69 return new Web3(new Web3.providers.HttpProvider(ENDPOINT));
70};
72const estimateGasLimit = async (utx: TransactionConfig) => {
73 const web3 = _web3();
74 const estimate = await web3.eth.estimateGas(utx);
75 return web3.utils
76 .toBN(estimate)
77 .muln(120)
78 .divn(100); // 20% cushion
79};
81const getMaxFeePerGas = async () => {
82 const web3 = _web3();
83 const fee = await web3.eth.getGasPrice();
84 return (Number(fee) * 1.2).toFixed(0); // 20% cushion
85};
87const signTransaction = async ({
88 wallet,
89 walletType,
90 walletHdPath, // TODO
91 networkType,
92 txn, // output of transactionBuilder in useEthereumTransaction
93 nonce, // number
94 chainId, // number
95 gasPriceData, // GasPriceData
96 gasLimit, // TODO: do we need the default values anymore? now that the estiamte is loaded dynamically
97 txnSigner, // optionally inject a transaction signing function,
98 txnSender, // and a sending function, for wallets that need these passed in.
99}: signTransactionProps) => {
100 const from = wallet.address;
101 const estimate = await estimateGasLimit({ ...txn, from });
102 const maxFeePerGas = await getMaxFeePerGas();
104 const txParams: EIP1559TxData = {
105 data: toHex(txn.data),
106 to: toHex(txn.to),
107 gasLimit: toHex(estimate),
108 maxFeePerGas: toHex(maxFeePerGas),
109 maxPriorityFeePerGas: toHex(
110 toWei(Math.round(gasPriceData.maxPriorityFeePerGas).toFixed(0), 'gwei')
111 ),
112 nonce: toHex(nonce),
113 chainId: toHex(chainId),
114 type: toHex(EIP1559_TRANSACTION_TYPE),
115 };
117 const chain =
118 networkType === NETWORK_TYPES.GOERLI ? Chain.Goerli : Chain.Mainnet;
120 const txConfig: TxOptions = {
121 freeze: false,
122 common: new Common({
123 chain,
124 hardfork: Hardfork.Merge,
125 }),
126 };
128 let stx = EIP1559Transaction.fromTxData(txParams, txConfig);
130 if (walletType === WALLET_TYPES.METAMASK) {
131 return metamaskSignTransaction(stx);
132 } else if (walletType === WALLET_TYPES.WALLET_CONNECT) {
133 if (!(txnSender && txnSigner)) {
134 throw Error('WalletConnect TX signer unavailable');
135 }
137 stx = await walletConnectSignTransaction({
138 from,
139 txn: stx,
140 txnSigner,
141 txnSender,
142 });
143 } else {
144 // BridgeWallet case
145 stx = stx.sign(wallet.privateKey!);
146 }
148 return stx;
149};
151const sendSignedTransaction = (
152 web3: Web3,
153 stx: EIP1559Transaction | FakeSignedTx,
154 doubtNonceError: boolean
155): Promise<string> => {
156 // if we couldn't sign it, we depend on the given sender function
157 if (!(stx instanceof EIP1559Transaction)) {
158 // TODO
159 if (doubtNonceError) {
160 console.log('why doubting nonce error? tank unavailable without rawtx.');
161 }
162 if (!stx.send) {
163 throw new Error('no sign+sending function available');
164 }
165 return stx.send(stx.txn, web3);
166 }
168 let rawTx: string = hexify(stx.serialize());
169 let eventEmitter = web3.eth.sendSignedTransaction(rawTx);
170 return new Promise(async (resolve, reject) => {
171 eventEmitter
172 .on('transactionHash', (hash: string) => {
173 resolve(hash);
174 })
175 .on('error', err => {
176 // if there's a nonce error, but we used the gas tank, it's likely
177 // that it's because the tank already submitted our transaction.
178 // we just wait for first confirmation here.
179 const message = typeof err === 'string' ? err : err.message || '';
180 const isKnownError =
181 message.includes('known transaction') ||
182 message.includes('already known');
183 const isNonceError =
184 message.includes("the tx doesn't have the correct nonce.") ||
185 message.includes('nonce too low');
186 if (rawTx && (isKnownError || (doubtNonceError && isNonceError))) {
187 console.log(
188 'tx send error likely from gas tank submission, ignoring:',
189 message
190 );
191 const txHash = web3.utils.keccak256(rawTx);
192 resolve(txHash);
193 } else {
194 console.error(err);
195 const wrappedErr = err instanceof Error ? err : new Error(message);
196 reject(wrappedErr);
197 }
198 });
199 });
200};
202// returns a Promise<void>, throwing on tx failure
203const waitForTransactionConfirm = (web3: Web3, txHash: string) => {
204 return retry(async (bail, n) => {
205 const receipt = await web3.eth.getTransactionReceipt(txHash);
206 const confirmed = receipt !== null;
207 if (!confirmed) {
208 throw new Error('Transaction not confirmed.');
209 }
211 const success = receipt.status === true;
212 if (!success) {
213 return bail(new Error('Transaction failed.'));
214 }
215 return receipt;
216 }, RETRY_OPTIONS);
217};
219// returns a Promise that resolves when all stxs have been sent & confirmed
220const sendAndAwaitAll = (
221 web3: Web3,
222 stxs: EIP1559Transaction[] | FakeSignedTx[],
223 doubtNonceError: boolean
224) => {
225 return Promise.all(
226 stxs.map(async tx => {
227 const txHash = await sendSignedTransaction(web3, tx, doubtNonceError);
228 return await waitForTransactionConfirm(web3, txHash);
229 })
230 );
231};
233const sendAndAwaitAllSerial = (
234 web3: Web3,
235 stxs: EIP1559Transaction[] | FakeSignedTx[],
236 doubtNonceError: boolean
237) => {
238 //@ts-expect-error tsc complains that stxs.reduce() is not callable
239 return stxs.reduce(
240 (
241 promise: Promise<TransactionReceipt[]>,
242 stx: EIP1559Transaction | FakeSignedTx
243 ) =>
244 promise.then(async (receipts = []) => {
245 const txHash = await sendSignedTransaction(web3, stx, doubtNonceError);
246 return [...receipts, await waitForTransactionConfirm(web3, txHash)];
247 }),
248 Promise.resolve()
249 );
250};
252const sendTransactionsAndAwaitConfirm = async (
253 web3: Web3,
254 signedTxs: EIP1559Transaction[] | FakeSignedTx[],
255 usedTank: boolean
256) =>
257 Promise.all(signedTxs.map(tx => sendSignedTransaction(web3, tx, usedTank)));
259const hexify = (val: string | Buffer) => addHexPrefix(val.toString('hex'));
261export {
262 RETRY_OPTIONS,
263 FakeSignResult,
264 signTransaction,
265 sendSignedTransaction,
266 waitForTransactionConfirm,
267 sendTransactionsAndAwaitConfirm,
268 sendAndAwaitAll,
269 sendAndAwaitAllSerial,
270 hexify,
271};