on df0c310 help sign in
bridge/src/lib/txn.ts
252 lines 7.9 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 const chain =
103 networkType === NETWORK_TYPES.GOERLI ? Chain.Goerli : Chain.Mainnet;
105 const txConfig: TxOptions = {
106 freeze: false,
107 common: new Common({
108 chain,
109 hardfork: Hardfork.Merge,
110 })
111 };
113 let stx = EIP1559Transaction.fromTxData(txParams, txConfig)
115 if (walletType === WALLET_TYPES.METAMASK) {
116 return metamaskSignTransaction(stx);
117 } else if (walletType === WALLET_TYPES.WALLET_CONNECT) {
118 if (!(txnSender && txnSigner)) {
119 throw Error('WalletConnect TX signer unavailable');
120 }
122 stx = await walletConnectSignTransaction({
123 from,
124 txn: stx,
125 txnSigner,
126 txnSender,
127 });
128 } else {
129 // BridgeWallet case
130 stx = stx.sign(wallet.privateKey!);
131 }
133 return stx;
134};
136const sendSignedTransaction = (
137 web3: Web3,
138 stx: EIP1559Transaction | FakeSignedTx,
139 doubtNonceError: boolean
140): Promise<string> => {
141 // if we couldn't sign it, we depend on the given sender function
142 if (!(stx instanceof EIP1559Transaction)) { // TODO
143 if (doubtNonceError) {
144 console.log('why doubting nonce error? tank unavailable without rawtx.');
145 }
146 if (!stx.send) {
147 throw new Error('no sign+sending function available');
148 }
149 return stx.send(stx.txn, web3);
150 }
152 let rawTx: string = hexify(stx.serialize());
153 let eventEmitter = web3.eth.sendSignedTransaction(rawTx);
154 return new Promise(async (resolve, reject) => {
155 eventEmitter
156 .on('transactionHash', (hash: string) => {
157 resolve(hash);
158 })
159 .on('error', err => {
160 // if there's a nonce error, but we used the gas tank, it's likely
161 // that it's because the tank already submitted our transaction.
162 // we just wait for first confirmation here.
163 const message = typeof err === 'string' ? err : err.message || '';
164 const isKnownError =
165 message.includes('known transaction') ||
166 message.includes('already known');
167 const isNonceError =
168 message.includes("the tx doesn't have the correct nonce.") ||
169 message.includes('nonce too low');
170 if (rawTx && (isKnownError || (doubtNonceError && isNonceError))) {
171 console.log(
172 'tx send error likely from gas tank submission, ignoring:',
173 message
174 );
175 const txHash = web3.utils.keccak256(rawTx);
176 resolve(txHash);
177 } else {
178 console.error(err);
179 const wrappedErr = err instanceof Error ? err : new Error(message);
180 reject(wrappedErr);
181 }
182 });
183 });
184};
186// returns a Promise<void>, throwing on tx failure
187const waitForTransactionConfirm = (web3: Web3, txHash: string) => {
188 return retry(async (bail, n) => {
189 const receipt = await web3.eth.getTransactionReceipt(txHash);
190 const confirmed = receipt !== null;
191 if (!confirmed) {
192 throw new Error('Transaction not confirmed.');
193 }
195 const success = receipt.status === true;
196 if (!success) {
197 return bail(new Error('Transaction failed.'));
198 }
199 return receipt;
200 }, RETRY_OPTIONS);
201};
203// returns a Promise that resolves when all stxs have been sent & confirmed
204const sendAndAwaitAll = (
205 web3: Web3,
206 stxs: EIP1559Transaction[] | FakeSignedTx[],
207 doubtNonceError: boolean
208) => {
209 return Promise.all(
210 stxs.map(async tx => {
211 const txHash = await sendSignedTransaction(web3, tx, doubtNonceError);
212 return await waitForTransactionConfirm(web3, txHash);
213 })
214 );
215};
217const sendAndAwaitAllSerial = (
218 web3: Web3,
219 stxs: EIP1559Transaction[] | FakeSignedTx[],
220 doubtNonceError: boolean
221) => {
222 //@ts-expect-error tsc complains that stxs.reduce() is not callable
223 return stxs.reduce(
224 (promise: Promise<TransactionReceipt[]>, stx: EIP1559Transaction | FakeSignedTx) =>
225 promise.then(async (receipts = []) => {
226 const txHash = await sendSignedTransaction(web3, stx, doubtNonceError);
227 return [...receipts, await waitForTransactionConfirm(web3, txHash)];
228 }),
229 Promise.resolve()
230 );
231};
233const sendTransactionsAndAwaitConfirm = async (
234 web3: Web3,
235 signedTxs: EIP1559Transaction[] | FakeSignedTx[],
236 usedTank: boolean
237) =>
238 Promise.all(signedTxs.map(tx => sendSignedTransaction(web3, tx, usedTank)));
240const hexify = (val: string | Buffer) => addHexPrefix(val.toString('hex'));
242export {
243 RETRY_OPTIONS,
244 FakeSignResult,
245 signTransaction,
246 sendSignedTransaction,
247 waitForTransactionConfirm,
248 sendTransactionsAndAwaitConfirm,
249 sendAndAwaitAll,
250 sendAndAwaitAllSerial,
251 hexify,
252};