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