on ea25a45 help sign in
bridge/src/lib/txn.ts
284 lines 8.2 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>;
63 web3: Web3;
66const estimateGasLimit = async (utx: TransactionConfig, web3: Web3) => {
67 const estimate = await web3.eth.estimateGas(utx);
68 return web3.utils
69 .toBN(estimate)
70 .muln(120)
71 .divn(100); // 20% cushion
72};
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 web3,
87}: signTransactionProps) => {
88 const from = wallet.address;
89 const estimate = await estimateGasLimit({ ...txn, from }, web3);
91 let block =
92 (await web3.eth.getBlock('pending')) || (await web3.eth.getBlock('latest'));
94 if (!block?.baseFeePerGas) {
95 throw new Error('Unable to fetch baseFeePerGas from the network');
96 }
98 const baseFeeWei = block.baseFeePerGas;
100 // from user dropdown
101 const maxPriorityFeePerGas = toWei(
102 String(Math.round(gasPriceData.maxPriorityFeePerGas)),
103 'gwei'
104 );
106 const maxFeePerGas = Math.floor(
107 (Number(baseFeeWei) + Number(maxPriorityFeePerGas)) * 1.2 // 20% cushion
108 );
110 const txParams: EIP1559TxData = {
111 data: toHex(txn.data),
112 to: toHex(txn.to),
113 gasLimit: toHex(estimate),
114 maxFeePerGas: toHex(maxFeePerGas),
115 maxPriorityFeePerGas: toHex(maxPriorityFeePerGas),
116 nonce: toHex(nonce),
117 chainId: toHex(chainId),
118 type: toHex(EIP1559_TRANSACTION_TYPE),
119 };
121 const chain =
122 networkType === NETWORK_TYPES.GOERLI || networkType === NETWORK_TYPES.LOCAL
123 ? Chain.Goerli
124 : Chain.Mainnet;
126 const common = new Common({
127 chain,
128 hardfork: Hardfork.Merge,
129 });
131 if (networkType === NETWORK_TYPES.LOCAL) {
132 common._chainParams.chainId = 1337;
133 common._chainParams.name = 'ganache';
134 }
136 const txConfig: TxOptions = {
137 freeze: false,
138 common: common,
139 };
141 let stx = EIP1559Transaction.fromTxData(txParams, txConfig);
143 if (walletType === WALLET_TYPES.METAMASK) {
144 return metamaskSignTransaction(stx);
145 } else if (walletType === WALLET_TYPES.WALLET_CONNECT) {
146 if (!(txnSender && txnSigner)) {
147 throw Error('WalletConnect TX signer unavailable');
148 }
150 stx = await walletConnectSignTransaction({
151 from,
152 txn: stx,
153 txnSigner,
154 txnSender,
155 });
156 } else {
157 // BridgeWallet case
158 stx = stx.sign(wallet.privateKey!);
159 }
161 return stx;
162};
164const sendSignedTransaction = (
165 web3: Web3,
166 stx: EIP1559Transaction | FakeSignedTx,
167 doubtNonceError: boolean
168): Promise<string> => {
169 // if we couldn't sign it, we depend on the given sender function
170 if (!(stx instanceof EIP1559Transaction)) {
171 // TODO
172 if (doubtNonceError) {
173 console.log('why doubting nonce error? tank unavailable without rawtx.');
174 }
175 if (!stx.send) {
176 throw new Error('no sign+sending function available');
177 }
178 return stx.send(stx.txn, web3);
179 }
181 let rawTx: string = hexify(stx.serialize());
182 let eventEmitter = web3.eth.sendSignedTransaction(rawTx);
183 return new Promise(async (resolve, reject) => {
184 eventEmitter
185 .on('transactionHash', (hash: string) => {
186 resolve(hash);
187 })
188 .on('error', err => {
189 // if there's a nonce error, but we used the gas tank, it's likely
190 // that it's because the tank already submitted our transaction.
191 // we just wait for first confirmation here.
192 const message = typeof err === 'string' ? err : err.message || '';
193 const isKnownError =
194 message.includes('known transaction') ||
195 message.includes('already known');
196 const isNonceError =
197 message.includes("the tx doesn't have the correct nonce.") ||
198 message.includes('nonce too low');
199 if (rawTx && (isKnownError || (doubtNonceError && isNonceError))) {
200 console.log(
201 'tx send error likely from gas tank submission, ignoring:',
202 message
203 );
204 const txHash = web3.utils.keccak256(rawTx);
205 resolve(txHash);
206 } else {
207 console.error(err);
208 const wrappedErr = err instanceof Error ? err : new Error(message);
209 reject(wrappedErr);
210 }
211 });
212 });
213};
215// returns a Promise<void>, throwing on tx failure
216const waitForTransactionConfirm = (web3: Web3, txHash: string) => {
217 return retry(async (bail, n) => {
218 const receipt = await web3.eth.getTransactionReceipt(txHash);
219 const confirmed = receipt !== null;
220 if (!confirmed) {
221 throw new Error('Transaction not confirmed.');
222 }
224 const success = receipt.status === true;
225 if (!success) {
226 return bail(new Error('Transaction failed.'));
227 }
228 return receipt;
229 }, RETRY_OPTIONS);
230};
232// returns a Promise that resolves when all stxs have been sent & confirmed
233const sendAndAwaitAll = (
234 web3: Web3,
235 stxs: EIP1559Transaction[] | FakeSignedTx[],
236 doubtNonceError: boolean
237) => {
238 return Promise.all(
239 stxs.map(async tx => {
240 const txHash = await sendSignedTransaction(web3, tx, doubtNonceError);
241 return await waitForTransactionConfirm(web3, txHash);
242 })
243 );
244};
246const sendAndAwaitAllSerial = (
247 web3: Web3,
248 stxs: EIP1559Transaction[] | FakeSignedTx[],
249 doubtNonceError: boolean
250) => {
251 //@ts-expect-error tsc complains that stxs.reduce() is not callable
252 return stxs.reduce(
253 (
254 promise: Promise<TransactionReceipt[]>,
255 stx: EIP1559Transaction | FakeSignedTx
256 ) =>
257 promise.then(async (receipts = []) => {
258 const txHash = await sendSignedTransaction(web3, stx, doubtNonceError);
259 return [...receipts, await waitForTransactionConfirm(web3, txHash)];
260 }),
261 Promise.resolve()
262 );
263};
265const sendTransactionsAndAwaitConfirm = async (
266 web3: Web3,
267 signedTxs: EIP1559Transaction[] | FakeSignedTx[],
268 usedTank: boolean
269) =>
270 Promise.all(signedTxs.map(tx => sendSignedTransaction(web3, tx, usedTank)));
272const hexify = (val: string | Buffer) => addHexPrefix(val.toString('hex'));
274export {
275 RETRY_OPTIONS,
276 FakeSignResult,
277 signTransaction,
278 sendSignedTransaction,
279 waitForTransactionConfirm,
280 sendTransactionsAndAwaitConfirm,
281 sendAndAwaitAll,
282 sendAndAwaitAllSerial,
283 hexify,
284};