diff --git a/README.md b/README.md index 3e2808a..bda1a4e 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ const tx: TransactionData = { }; const feeOracle = new TornadoFeeOracleV5(1, 'https://eth.llamarpc.com'); // First parameter - chain ID -const withdrawalGas = await feeOracle.getGas(tx, 'relayer_withdrawal'); +const withdrawalGas = await feeOracle.getGas({tx, txType: 'relayer_withdrawal'}); ``` ##### Estimate gas price and gas limit to send transaction @@ -51,10 +51,9 @@ const incompleteTx: TransactionData = { const feeOracle = new TornadoFeeOracleV5(1, 'https://eth.llamarpc.com'); const transactionType: TxType = 'relayer_withdrawal'; -const gasPrice = await feeOracle.getGasPrice(transactionType); -const gasLimit = await feeOracle.getGasLimit(incompleteTx, transactionType); +const { gasPrice, gasLimit } = await feeOracle.getGasParams({tx: incompleteTx, txType: transactionType}); -const tx: TransactionData = Object.assign({ gasPrice, gasLimit }, incompleteTx); +const tx: TransactionData = {...incompleteTx, gasPrice, gasLimit} ``` ##### Get token prices (rate to ETH) for tokens that used in Tornado diff --git a/package.json b/package.json index d77ffda..ecac8ec 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tornado/tornado-oracles", - "version": "2.1.0", - "description": "Gas oracle for Tornado-specific transactions", + "version": "3.0.0", + "description": "Oracles for Tornado-specific transactions & actions", "main": "./lib/index.js", "types": "./lib/index.d.ts", "scripts": { diff --git a/src/feeOracle.ts b/src/feeOracle.ts index aa8e2d8..d5b2737 100644 --- a/src/feeOracle.ts +++ b/src/feeOracle.ts @@ -1,5 +1,6 @@ import { GasPriceOracle } from '@tornado/gas-price-oracle'; import { BigNumber, BigNumberish, ethers } from 'ethers'; +import BigNumberFloat from 'bignumber.js'; import { parseUnits } from 'ethers/lib/utils'; import { TransactionData, @@ -9,12 +10,14 @@ import { GasPriceParams, GetGasParamsRes, HexadecimalStringifiedNumber, + GetGasInput, + GetGasParamsInput, } from './types'; import { JsonRpcProvider } from '@ethersproject/providers'; import { ChainId, defaultGasPrices, defaultInstanceTokensGasLimit, InstanceTokenSymbol } from './config'; import { bump, calculateGasPriceInWei, convertETHToToken, fromGweiToWeiHex, serializeTx } from './utils'; import { getOptimismL1FeeOracle } from './contracts/factories'; -import { AvailableTokenSymbols } from '@tornado/tornado-config'; +import { GetWithdrawalFeeViaRelayerInput } from './types'; export abstract class TornadoFeeOracle implements ITornadoFeeOracle { protected provider: JsonRpcProvider; @@ -44,55 +47,71 @@ export abstract class TornadoFeeOracle implements ITornadoFeeOracle { /** * Estimate gas price, gas limit and l1Fee for sidechain (if exists) - * @param {TransactionData} [tx] Transaction data in web3 / ethers format - * @param {TxType} [txType=other] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other' - * @param {number} [bumpGasLimitPercent] Gas limit bump percent to prioritize transaction (recenlty used) - * @param {number} [bumpGasPricePercent] Gas price bump percent to prioritize transaction (rarely used) - * @param {LegacyGasPriceKey} [speed] Preferred transaction speed, if uses legacy gas (before EIP-1559) - * @returns {Promise} Object with fields 'gasPrice', 'gasLimit' and 'l1Fee' + * @param {GetGasParamsInput} params Function input arguments object + * @param {TransactionData} [params.tx] Transaction data in web3 / ethers format + * @param {TxType} [params.txType=other] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other' + * @param {number} [params.predefinedGasLimit] Predefined gas limit, if already calculated (no refetching) + * @param {number} [params.predefinedGasPrice] Predefined gas price, if already calculated (no refetching) + * @param {number} [params.bumpGasLimitPercent] Gas limit bump percent to prioritize transaction (if gas limit not predefined, recenlty used) + * @param {number} [params.bumpGasPricePercent] Gas price bump percent to prioritize transaction (if gas limit not predefined, rarely used) + * @param {LegacyGasPriceKey} [params.speed] Preferred transaction speed, if uses legacy gas (before EIP-1559) + * @param {boolean} [params.includeL1FeeToGasLimit=true] Include L1 additional fee on Optimism to gas limit (get fee and divide by gas price) + * @returns {Promise} Object with fields 'gasPrice' and 'gasLimit', L1 fee, if exists, included in gasLimit */ - async getGasParams( - tx?: TransactionData, - txType: TxType = 'other', - bumpGasLimitPercent?: number, - bumpGasPricePercent?: number, - speed?: LegacyGasPriceKey, - ): Promise { - const [gasPrice, gasLimit, l1Fee] = await Promise.all([ - this.getGasPrice(speed, bumpGasPricePercent), - this.getGasLimit(tx, txType, bumpGasLimitPercent), - this.fetchL1OptimismFee(tx), - ]); + async getGasParams(params: GetGasParamsInput = {}): Promise { + let { + tx, + txType = 'other', + bumpGasLimitPercent, + bumpGasPricePercent, + predefinedGasLimit: gasLimit, + predefinedGasPrice: gasPrice, + speed, + includeL1FeeToGasLimit = true, + } = params; - return { gasLimit, gasPrice, l1Fee }; + let l1Fee: string = '0'; + if (!gasLimit && !gasPrice) { + [gasPrice, gasLimit, l1Fee] = await Promise.all([ + this.getGasPrice(speed, bumpGasPricePercent), + this.getGasLimit(tx, txType, bumpGasLimitPercent), + this.fetchL1OptimismFee(tx), + ]); + } + if (!gasLimit) { + [gasLimit, l1Fee] = await Promise.all([ + this.getGasLimit(tx, txType, bumpGasLimitPercent), + this.fetchL1OptimismFee(tx), + ]); + } + if (!gasPrice) gasPrice = await this.getGasPrice(speed, bumpGasPricePercent); + + if (includeL1FeeToGasLimit) + // Include L1 fee in gas limit (divide by gas price before), if l1 fee is 0, gas limit wont change + gasLimit = BigNumberFloat(gasLimit) + .plus(BigNumberFloat(l1Fee).div(BigNumberFloat(gasPrice))) + .decimalPlaces(0, 1) + .toNumber(); + + return { gasLimit, gasPrice }; } /** * Estimates next block gas for signed, unsigned or incomplete Tornado transaction - * @param {TransactionData} [tx] Transaction data in web3 / ethers format - * @param {TxType} [txType=other] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other' - * @param {number} [bumpGasLimitPercent] Gas limit bump percent to prioritize transaction (recenlty used) - * @param {number} [bumpGasPricePercent] Gas price bump percent to prioritize transaction (rarely used) - * @param {LegacyGasPriceKey} [speed] Preferred transaction speed, if uses legacy gas (before EIP-1559) + * @param {GetGasInput} params Function input arguments object + * @param {TransactionData} [params.tx] Transaction data in web3 / ethers format + * @param {TxType} [params.txType] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other' + * @param {number} [params.predefinedGasLimit] Predefined gas limit, if already calculated (no refetching) + * @param {number} [params.predefinedGasPrice] Predefined gas price, if already calculated (no refetching) + * @param {number} [params.bumpGasLimitPercent] Gas limit bump percent to prioritize transaction (if gas limit not predefined, recenlty used) + * @param {number} [params.bumpGasPricePercent] Gas price bump percent to prioritize transaction (if gas price not predefined, rarely used) + * @param {LegacyGasPriceKey} [params.speed] Preferred transaction speed, if uses legacy gas (before EIP-1559) * @returns {Promise} Gas value in WEI (hex-format) */ - async getGas( - tx?: TransactionData, - txType: TxType = 'other', - bumpGasLimitPercent?: number, - bumpGasPricePercent?: number, - speed?: LegacyGasPriceKey, - ): Promise { - const { gasPrice, gasLimit, l1Fee } = await this.getGasParams( - tx, - txType, - bumpGasLimitPercent, - bumpGasPricePercent, - speed, - ); - const gas = BigNumber.from(gasPrice).mul(gasLimit).add(l1Fee); + async getGas(params: GetGasInput = {}): Promise { + const { gasPrice, gasLimit } = await this.getGasParams({ ...params, includeL1FeeToGasLimit: true }); - return gas.toHexString(); + return BigNumber.from(gasPrice).mul(gasLimit).toHexString(); } /** @@ -138,61 +157,98 @@ export abstract class TornadoFeeOracle implements ITornadoFeeOracle { * and if the relayer pays a commission and the transfer of tokens fails, this commission will remain to the relayer. * * Refund needed that recipient can use tokens after withdrawal (covers gas fee for send/swap) + * @param {BigNumberish} gasPrice Actual gas price * @param {InstanceTokenSymbol} tokenSymbol Withdrawal token (currency) symbol - for example, 'dai' - * @returns {Promise} Refund amount in WEI (in hex format) + * @returns {HexadecimalStringifiedNumber} Refund amount in WEI (in hex format) */ - async calculateRefundInETH(tokenSymbol: InstanceTokenSymbol): Promise { + calculateRefundInETH(gasPrice: BigNumberish, tokenSymbol: InstanceTokenSymbol): HexadecimalStringifiedNumber { // In Tornado we need to calculate refund only on user side, relayer get refund value in proof - const gasPrice = await this.getGasPrice(); const gasLimit = defaultInstanceTokensGasLimit[tokenSymbol]; return BigNumber.from(gasPrice).mul(gasLimit).mul(2).toHexString(); } + /** + * Fetched actual gas price and calculates refund amount + * @param {InstanceTokenSymbol} tokenSymbol Withdrawal token (currency) symbol - for example, 'dai' + * @returns {Promise} Refund amount in WEI (in hex format) + */ + async fetchRefundInETH(tokenSymbol: InstanceTokenSymbol): Promise { + const gasPrice = await this.getGasPrice(); + return this.calculateRefundInETH(gasPrice, tokenSymbol); + } + /** * Get refund amount on ETH or Goerli in non-native token - * @param {BigNumberish} tokenPriceInEth Token price in WEI in native currency + * @param {BigNumberish} gasPrice Actual gas price in ETH + * @param {BigNumberish} tokenPriceInEth Token price in WEI in ETH * @param {HexadecimalStringifiedNumber | number} tokenDecimals Token (currency) decimals * @param {InstanceTokenSymbol} tokenSymbol Withdrawal token (currency) symbol - for example, 'dai' - * @returns {Promise} Refund amount in WEI in selected token (hexed number) + * @returns {HexadecimalStringifiedNumber} Refund amount in WEI in selected token (hexed number) */ - async calculateRefundInToken( + calculateRefundInToken( + gasPrice: BigNumberish, tokenPriceInEth: BigNumberish, tokenDecimals: HexadecimalStringifiedNumber | number, tokenSymbol: InstanceTokenSymbol, - ): Promise { - const refundInEth = await this.calculateRefundInETH(tokenSymbol); + ): HexadecimalStringifiedNumber { + const refundInEth = this.calculateRefundInETH(gasPrice, tokenSymbol); return convertETHToToken(refundInEth, tokenDecimals, tokenPriceInEth).toHexString(); } + /** + * Calculates relayer fee in selected currency (ETH, DAI, BNB etc) in WEI + * @param {number | string} relayerFeePercent Relayer percent (0.4 for ETH Mainnet, for example) + * @param {HexadecimalStringifiedNumber | number} amount Amount in selected currency (10 for 10 ETH, 1000 for 1000 DAI) + * @param {string | number} decimals Decimal places in selected token (currency) + * @returns {HexadecimalStringifiedNumber} Fee in WEI (hexed stingified number) + */ + calculateRelayerFeeInWei( + relayerFeePercent: number | string, + amount: HexadecimalStringifiedNumber | number, + decimals: string | number, + ): HexadecimalStringifiedNumber { + return parseUnits(amount.toString(), decimals) + .mul(`${Math.floor(Number(relayerFeePercent) * 1e10)}`) + .div(`${100 * 1e10}`) + .toHexString(); + } + /** * Estimates fee for withdrawal via relayer depending on type: gas bump percent is bigger, if it calculates by user, * so that the real commission from the relayer side is a little less, * in order to the relayer can send a transaction without fear that he will go into the red - * @param {TxType} type Tornado transaction type: withdrawal costs calculation from user side or from relayer side - * @param {TransactionData} tx Transaction data (object in web3 / ethers format) - * @param {number} relayerFeePercent Relayer fee percent from the transaction amount (for example, 0.15 for BNB or 0.4 for ETH Mainnet) - * @param {AvailableTokenSymbols | Uppercase} currency Currency symbol - * @param {number | HexadecimalStringifiedNumber } amount Withdrawal amount in selected currency - * @param {number | HexadecimalStringifiedNumber } decimals Token (currency) decimals - * @param {BigNumberish} [refundInEth=0] Refund in ETH, if withdrawed other tokens on Mainnet (not ETH) - * @param {BigNumberish} [tokenPriceInEth] If withdrawing other token on Mainnet or Goerli, need to provide token price in ETH (in WEI) + * @param {GetWithdrawalFeeViaRelayerInput} params Function input arguments object + * @param {TxType} params.type Tornado transaction type: withdrawal costs calculation from user side or from relayer side + * @param {TransactionData} [params.tx] Transaction data (object in web3 / ethers format) + * @param {number} params.relayerFeePercent Relayer fee percent from the transaction amount (for example, 0.15 for BNB or 0.4 for ETH Mainnet) + * @param {AvailableTokenSymbols | Uppercase} params.currency Currency symbol + * @param {number | HexadecimalStringifiedNumber } params.amount Withdrawal amount in selected currency + * @param {number | HexadecimalStringifiedNumber } params.decimals Token (currency) decimals + * @param {BigNumberish} [params.refundInEth=0] Refund in ETH, if withdrawed other tokens on Mainnet (not ETH) + * @param {BigNumberish} [params.tokenPriceInEth] If withdrawing other token on Mainnet or Goerli, need to provide token price in ETH (in WEI) + * @param {number} [params.gasLimit] Predefined gas limit, if already calculated (no refetching) + * @param {number} [params.gasPrice] Predefined gas price, if already calculated (no refetching) + * * @returns {Promise} Fee in WEI (hexed string) */ async calculateWithdrawalFeeViaRelayer( - type: TxType, - tx: TransactionData, - relayerFeePercent: number, - currency: AvailableTokenSymbols | Uppercase, - amount: HexadecimalStringifiedNumber | number, - decimals: HexadecimalStringifiedNumber | number, - refundInEth: BigNumberish = 0, - tokenPriceInEth?: BigNumberish, + params: GetWithdrawalFeeViaRelayerInput, ): Promise { - const gasCosts = BigNumber.from(await this.getGas(tx, type)); + let { + tx, + txType, + relayerFeePercent, + currency, + amount, + decimals, + refundInEth = 0, + tokenPriceInEth, + gasLimit, + gasPrice, + } = params; - const relayerFee = parseUnits(amount.toString(), decimals) - .mul(`${Math.floor(relayerFeePercent * 1e10)}`) - .div(`${100 * 1e10}`); + const relayerFee = this.calculateRelayerFeeInWei(relayerFeePercent, amount, decimals); + const gasCosts = await this.getGas({ tx, txType, predefinedGasLimit: gasLimit, predefinedGasPrice: gasPrice }); if ((this.chainId === ChainId.MAINNET || this.chainId === ChainId.GOERLI) && currency.toLowerCase() != 'eth') { if (!tokenPriceInEth) { @@ -200,10 +256,10 @@ export abstract class TornadoFeeOracle implements ITornadoFeeOracle { return '0'; } - const feeInEth = gasCosts.add(refundInEth); + const feeInEth = BigNumber.from(gasCosts).add(refundInEth); return convertETHToToken(feeInEth, decimals, tokenPriceInEth).add(relayerFee).toHexString(); } - return gasCosts.add(relayerFee).toHexString(); + return BigNumber.from(gasCosts).add(relayerFee).toHexString(); } } diff --git a/src/feeOracleV4.ts b/src/feeOracleV4.ts index 8f51ebd..f710352 100644 --- a/src/feeOracleV4.ts +++ b/src/feeOracleV4.ts @@ -26,7 +26,7 @@ export class TornadoFeeOracleV4 extends TornadoFeeOracle implements ITornadoFeeO // For compatibility reasons, when wee check user-provided fee for V4 withdrawal transaction, we need dump gas limit // for about 20 percent,so that the transaction will be sent, even if it results in some loss for the relayer if (type === 'relayer_withdrawal_check_v4') return bump(defaultWithdrawalGasLimit[this.chainId], -25).toNumber(); - if (!tx || Object.keys(tx).length === 0) return bump(21_000, bumpPercent).toNumber(); + if (!tx || Object.keys(tx).length === 0) return bump(23_000, bumpPercent).toNumber(); return bump(await this.provider.estimateGas(tx), bumpPercent).toNumber(); } diff --git a/src/types.ts b/src/types.ts index 5f7f089..ebdd709 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,7 +18,8 @@ export type LegacyGasPrices = { - 'relayer_withdrawal_check_v4' - Fee calculation on relayer side, when V4 relayer checks user-provided fee. For compatibility reasons - 'other' - Any other non-specific transaction */ -export type TxType = 'user_withdrawal' | 'relayer_withdrawal' | 'relayer_withdrawal_check_v4' | 'other'; +export type WithdrawalTxType = 'user_withdrawal' | 'relayer_withdrawal' | 'relayer_withdrawal_check_v4'; +export type TxType = WithdrawalTxType | 'other'; export interface TransactionData { to: string; @@ -35,40 +36,21 @@ export interface TransactionData { } export interface ITornadoFeeOracle { - getGasParams: ( - tx?: TransactionData, - txType?: TxType, - bumpGasLimitPercent?: number, - bumpGasPricePercent?: number, - speed?: LegacyGasPriceKey, - ) => Promise; - getGas: ( - tx?: TransactionData, - type?: TxType, - bumpGasLimitPercent?: number, - bumpGasPricePercent?: number, - speed?: LegacyGasPriceKey, - ) => Promise; + getGasParams: (params?: GetGasParamsInput) => Promise; + getGas: (params?: GetGasInput) => Promise; getGasPriceParams: (speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise; getGasPrice: (speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise; getGasLimit: (tx?: TransactionData, type?: TxType, bumpPercent?: number) => Promise; fetchL1OptimismFee: (tx?: TransactionData) => Promise; - calculateRefundInETH: (tokenSymbol: InstanceTokenSymbol) => Promise; + calculateRefundInETH: (gasPrice: BigNumberish, tokenSymbol: InstanceTokenSymbol) => HexadecimalStringifiedNumber; + fetchRefundInETH: (tokenSymbol: InstanceTokenSymbol) => Promise; calculateRefundInToken: ( + gasPrice: BigNumberish, tokenPriceInEth: BigNumberish, tokenDecimals: HexadecimalStringifiedNumber | number, tokenSymbol: InstanceTokenSymbol, - ) => Promise; - calculateWithdrawalFeeViaRelayer: ( - type: TxType, - tx: TransactionData, - relayerFeePercent: number, - currency: AvailableTokenSymbols, - amount: HexadecimalStringifiedNumber | number, - decimals: number, - refundInEth: BigNumberish, - tokenPriceInEth?: BigNumberish, - ) => Promise; + ) => HexadecimalStringifiedNumber; + calculateWithdrawalFeeViaRelayer: (params: GetWithdrawalFeeViaRelayerInput) => Promise; } export interface ITornadoPriceOracle { @@ -99,6 +81,34 @@ export type TokenPrices = { [tokenSymbol in TokenSymbol]?: BigNumberish }; // Reponse type for getGasParams function of fee oracle export type GetGasParamsRes = { gasLimit: number; - gasPrice: HexadecimalStringifiedNumber; - l1Fee: HexadecimalStringifiedNumber; + gasPrice: HexadecimalStringifiedNumber; // Gas price in native currency +}; + +export type GetGasInput = { + // Transaction type: user-side calculation, relayer-side calculation or + // relayer calculation to check user-provided fee in old V4 relayer (for backwards compatibility) + txType?: TxType; + tx?: TransactionData; // Transaction data in ethers format + predefinedGasPrice?: HexadecimalStringifiedNumber; // Predefined gas price for withdrawal tx (wont be calculated again in function) + predefinedGasLimit?: number; // Predefined gas limit for withdrawal tx (wont be calculated again in function) + bumpGasLimitPercent?: number; // Gas limit bump percent to prioritize transaction (recenlty used) + bumpGasPricePercent?: number; // Gas price bump percent to prioritize transaction (rarely used) + speed?: LegacyGasPriceKey; // Preferred transaction speed, if uses legacy gas (before EIP-1559) +}; + +export type GetGasParamsInput = GetGasInput & { includeL1FeeToGasLimit?: boolean }; + +export type GetWithdrawalFeeViaRelayerInput = { + // Transaction type: user-side calculation, relayer-side calculation or + // relayer calculation to check user-provided fee in old V4 relayer (for backwards compatibility) + txType: WithdrawalTxType; + tx?: TransactionData; // Transaction data in ethers format + relayerFeePercent: number | string; // Relayer fee percent from withdrawal amount (for example, 0.15 for BNB or 0.4 for ETH Mainnet) + currency: AvailableTokenSymbols | Uppercase; // Currency (token) symbol + amount: string | number; // Withdrawal amount in selected currency (10 for 10 ETH, 10000 for 10000 DAI) + decimals: string | number; // Token (currency) decimal places + refundInEth?: HexadecimalStringifiedNumber; // Refund amount in ETH, if withdrawing non-native currency on ETH Mainnet or Goerli + tokenPriceInEth?: HexadecimalStringifiedNumber | string; // Token (currency) price in ETH wei, if withdrawing non-native currency + gasPrice?: HexadecimalStringifiedNumber; // Predefined gas price for withdrawal tx (wont be calculated again in function) + gasLimit?: number; // Predefined gas limit for withdrawal tx (wont be calculated again in function) };