Compare commits

..

8 Commits
2.1.0 ... main

Author SHA1 Message Date
bf6a320fbc yarn.lock mistyping fixed 2024-07-26 20:33:59 -04:00
004d83e547 updated repository, gitea and npm urls, and yarn.lock file related parameters 2024-07-26 20:27:45 -04:00
9def6cd536 Fix & extend JSDoc notes 2023-09-06 17:38:12 -07:00
65af25e76b Return zero value from calculateRefundInETH function if called on incorrect chain or with native token or tokenSymbol is invalid & bump package version to 3.3.0 2023-09-06 15:43:44 -07:00
2ce8cafdbd Calculate refund on user side only if no value provided (strictly undefined) 2023-09-06 14:21:12 -07:00
394d6cab5c Add public field version to oracle class 2023-09-06 14:07:35 -07:00
9ef88948d2 Calculate refund in calculateWithdrawalFeeViaRelayer, if correct value didn't provided when calculating for user_withdrawal transaction type & bump to 3.1.0 2023-09-04 13:50:49 -07:00
be7f911a17 Update package to version 3.0.0:
- Simplify getGasParams function by including additional L1 fee in gasLimit
	- Make calculateRefund functions non-async by providing gas price (its ok, because refund needs to be calculated only on user side, where gas price is known)
	- Allow passing to calculateWithdrawalFeeViaRelayer function predefined gas price and gas limit to not refetch/recalculate those values
	- Pass arguments as objects to functions with many optional parameters
2023-09-04 13:40:29 -07:00
8 changed files with 211 additions and 124 deletions

View File

@ -35,7 +35,7 @@ const tx: TransactionData = {
}; };
const feeOracle = new TornadoFeeOracleV5(1, 'https://eth.llamarpc.com'); // First parameter - chain ID 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 ##### 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 feeOracle = new TornadoFeeOracleV5(1, 'https://eth.llamarpc.com');
const transactionType: TxType = 'relayer_withdrawal'; const transactionType: TxType = 'relayer_withdrawal';
const gasPrice = await feeOracle.getGasPrice(transactionType); const { gasPrice, gasLimit } = await feeOracle.getGasParams({tx: incompleteTx, txType: transactionType});
const gasLimit = await feeOracle.getGasLimit(incompleteTx, 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 ##### Get token prices (rate to ETH) for tokens that used in Tornado

View File

@ -1,7 +1,7 @@
{ {
"name": "@tornado/tornado-oracles", "name": "@tornado/tornado-oracles",
"version": "2.1.0", "version": "3.3.0",
"description": "Gas oracle for Tornado-specific transactions", "description": "Oracles for Tornado-specific transactions & actions",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"scripts": { "scripts": {
@ -11,7 +11,8 @@
"prettier:fix": "prettier --write . --config .prettierrc", "prettier:fix": "prettier --write . --config .prettierrc",
"build:abi": "yarn typechain --target ethers-v5 --out-dir src/contracts ./abis/*.abi.json", "build:abi": "yarn typechain --target ethers-v5 --out-dir src/contracts ./abis/*.abi.json",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"prepare": "npm run build && npm run build:esm" "prepare": "npm run build && npm run build:esm",
"prepublish": "npm run prettier:fix"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,5 +1,6 @@
import { GasPriceOracle } from '@tornado/gas-price-oracle'; import { GasPriceOracle } from '@tornado/gas-price-oracle';
import { BigNumber, BigNumberish, ethers } from 'ethers'; import { BigNumber, BigNumberish, ethers } from 'ethers';
import BigNumberFloat from 'bignumber.js';
import { parseUnits } from 'ethers/lib/utils'; import { parseUnits } from 'ethers/lib/utils';
import { import {
TransactionData, TransactionData,
@ -9,17 +10,21 @@ import {
GasPriceParams, GasPriceParams,
GetGasParamsRes, GetGasParamsRes,
HexadecimalStringifiedNumber, HexadecimalStringifiedNumber,
GetGasInput,
GetGasParamsInput,
} from './types'; } from './types';
import { JsonRpcProvider } from '@ethersproject/providers'; import { JsonRpcProvider } from '@ethersproject/providers';
import { ChainId, defaultGasPrices, defaultInstanceTokensGasLimit, InstanceTokenSymbol } from './config'; import { ChainId, defaultGasPrices, defaultInstanceTokensGasLimit, InstanceTokenSymbol } from './config';
import { bump, calculateGasPriceInWei, convertETHToToken, fromGweiToWeiHex, serializeTx } from './utils'; import { bump, calculateGasPriceInWei, convertETHToToken, fromGweiToWeiHex, serializeTx } from './utils';
import { getOptimismL1FeeOracle } from './contracts/factories'; import { getOptimismL1FeeOracle } from './contracts/factories';
import { GetWithdrawalFeeViaRelayerInput } from './types';
import { AvailableTokenSymbols } from '@tornado/tornado-config'; import { AvailableTokenSymbols } from '@tornado/tornado-config';
export abstract class TornadoFeeOracle implements ITornadoFeeOracle { export abstract class TornadoFeeOracle implements ITornadoFeeOracle {
protected provider: JsonRpcProvider; protected provider: JsonRpcProvider;
public constructor( public constructor(
public version: 4 | 5,
protected chainId: ChainId, protected chainId: ChainId,
rpcUrl: string, rpcUrl: string,
protected oracle: GasPriceOracle, protected oracle: GasPriceOracle,
@ -30,7 +35,7 @@ export abstract class TornadoFeeOracle implements ITornadoFeeOracle {
/** /**
* Because Optimism transaction published on Mainnet, for each OP transaction we need to calculate L1 security fee: * Because Optimism transaction published on Mainnet, for each OP transaction we need to calculate L1 security fee:
* https://community.optimism.io/docs/developers/build/transaction-fees/#priority-fee * https://community.optimism.io/docs/developers/build/transaction-fees/#priority-fee
* @param {TransactionData} tx Transaction data to estimate L1 additional fee * @param {TransactionData} [tx] Transaction data to estimate L1 additional fee
* @returns {Promise<HexadecimalStringifiedNumber>} Fee in WEI (MATIC), '0' if chain is not Optimism * @returns {Promise<HexadecimalStringifiedNumber>} Fee in WEI (MATIC), '0' if chain is not Optimism
*/ */
async fetchL1OptimismFee(tx?: TransactionData): Promise<HexadecimalStringifiedNumber> { async fetchL1OptimismFee(tx?: TransactionData): Promise<HexadecimalStringifiedNumber> {
@ -44,55 +49,71 @@ export abstract class TornadoFeeOracle implements ITornadoFeeOracle {
/** /**
* Estimate gas price, gas limit and l1Fee for sidechain (if exists) * Estimate gas price, gas limit and l1Fee for sidechain (if exists)
* @param {TransactionData} [tx] Transaction data in web3 / ethers format * @param {GetGasParamsInput} [params] Function input arguments object
* @param {TxType} [txType=other] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other' * @param {TransactionData} [params.tx] Transaction data in web3 / ethers format
* @param {number} [bumpGasLimitPercent] Gas limit bump percent to prioritize transaction (recenlty used) * @param {TxType} [params.txType=other] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other'
* @param {number} [bumpGasPricePercent] Gas price bump percent to prioritize transaction (rarely used) * @param {number} [params.predefinedGasLimit] Predefined gas limit, if already calculated (no refetching)
* @param {LegacyGasPriceKey} [speed] Preferred transaction speed, if uses legacy gas (before EIP-1559) * @param {number} [params.predefinedGasPrice] Predefined gas price, if already calculated (no refetching)
* @returns {Promise<GetGasParamsRes>} Object with fields 'gasPrice', 'gasLimit' and 'l1Fee' * @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<GetGasParamsRes>} Object with fields 'gasPrice' and 'gasLimit', L1 fee, if exists, included in gasLimit
*/ */
async getGasParams( async getGasParams(params: GetGasParamsInput = {}): Promise<GetGasParamsRes> {
tx?: TransactionData, let {
txType: TxType = 'other', tx,
bumpGasLimitPercent?: number, txType = 'other',
bumpGasPricePercent?: number, bumpGasLimitPercent,
speed?: LegacyGasPriceKey, bumpGasPricePercent,
): Promise<GetGasParamsRes> { predefinedGasLimit: gasLimit,
const [gasPrice, gasLimit, l1Fee] = await Promise.all([ predefinedGasPrice: gasPrice,
this.getGasPrice(speed, bumpGasPricePercent), speed,
this.getGasLimit(tx, txType, bumpGasLimitPercent), includeL1FeeToGasLimit = true,
this.fetchL1OptimismFee(tx), } = 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 * Estimates next block gas for signed, unsigned or incomplete Tornado transaction
* @param {TransactionData} [tx] Transaction data in web3 / ethers format * @param {GetGasInput} [params] Function input arguments object
* @param {TxType} [txType=other] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other' * @param {TransactionData} [params.tx] Transaction data in web3 / ethers format
* @param {number} [bumpGasLimitPercent] Gas limit bump percent to prioritize transaction (recenlty used) * @param {TxType} [params.txType] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other'
* @param {number} [bumpGasPricePercent] Gas price bump percent to prioritize transaction (rarely used) * @param {number} [params.predefinedGasLimit] Predefined gas limit, if already calculated (no refetching)
* @param {LegacyGasPriceKey} [speed] Preferred transaction speed, if uses legacy gas (before EIP-1559) * @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<HexadecimalStringifiedNumber>} Gas value in WEI (hex-format) * @returns {Promise<HexadecimalStringifiedNumber>} Gas value in WEI (hex-format)
*/ */
async getGas( async getGas(params: GetGasInput = {}): Promise<HexadecimalStringifiedNumber> {
tx?: TransactionData, const { gasPrice, gasLimit } = await this.getGasParams({ ...params, includeL1FeeToGasLimit: true });
txType: TxType = 'other',
bumpGasLimitPercent?: number,
bumpGasPricePercent?: number,
speed?: LegacyGasPriceKey,
): Promise<HexadecimalStringifiedNumber> {
const { gasPrice, gasLimit, l1Fee } = await this.getGasParams(
tx,
txType,
bumpGasLimitPercent,
bumpGasPricePercent,
speed,
);
const gas = BigNumber.from(gasPrice).mul(gasLimit).add(l1Fee);
return gas.toHexString(); return BigNumber.from(gasPrice).mul(gasLimit).toHexString();
} }
/** /**
@ -114,8 +135,8 @@ export abstract class TornadoFeeOracle implements ITornadoFeeOracle {
/** /**
* Estimate next block gas price * Estimate next block gas price
* @param {LegacyGasPriceKey} speed Preferred transaction speed, if uses legacy gas (before EIP-1559) * @param {LegacyGasPriceKey} [speed] Preferred transaction speed, if uses legacy gas (before EIP-1559)
* @param {number} bumpPercent Gas bump percent to prioritize transaction * @param {number} [bumpPercent] Gas bump percent to prioritize transaction
* @returns {Promise<HexadecimalStringifiedNumber>} Gas price in WEI (hex string) * @returns {Promise<HexadecimalStringifiedNumber>} Gas price in WEI (hex string)
*/ */
async getGasPrice(speed?: LegacyGasPriceKey, bumpPercent?: number): Promise<HexadecimalStringifiedNumber> { async getGasPrice(speed?: LegacyGasPriceKey, bumpPercent?: number): Promise<HexadecimalStringifiedNumber> {
@ -125,9 +146,9 @@ export abstract class TornadoFeeOracle implements ITornadoFeeOracle {
/** /**
* Estimates gas limit for transaction (or basic gas limit, if no tx data provided) * Estimates gas limit for transaction (or basic gas limit, if no tx data provided)
* @param {TransactionData} tx Transaction data (object in web3 / ethers format) * @param {TransactionData} [tx] Transaction data (object in web3 / ethers format)
* @param {TxType} type Tornado transaction type: withdrawal by user, withdrawal by relayer, relayer fee check or 'other' * @param {TxType} [type] Tornado transaction type: withdrawal by user, withdrawal by relayer, relayer fee check or 'other'
* @param {number} bumpPercent Gas bump percent to prioritize transaction * @param {number} [bumpPercent] Gas bump percent to prioritize transaction
* @returns {Promise<number>} Gas limit * @returns {Promise<number>} Gas limit
*/ */
abstract getGasLimit(tx?: TransactionData, type?: TxType, bumpPercent?: number): Promise<number>; abstract getGasLimit(tx?: TransactionData, type?: TxType, bumpPercent?: number): Promise<number>;
@ -138,61 +159,107 @@ 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. * 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) * 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' * @param {InstanceTokenSymbol} tokenSymbol Withdrawal token (currency) symbol - for example, 'dai'
* @returns {Promise<HexadecimalStringifiedNumber>} Refund amount in WEI (in hex format) * @returns {HexadecimalStringifiedNumber} Refund amount in WEI (in hex format)
*/ */
async calculateRefundInETH(tokenSymbol: InstanceTokenSymbol): Promise<HexadecimalStringifiedNumber> { calculateRefundInETH(gasPrice: BigNumberish, tokenSymbol: InstanceTokenSymbol): HexadecimalStringifiedNumber {
// Refund only available for non-native tokens on Ethereum Mainnet and Goerli
if (![ChainId.MAINNET, ChainId.GOERLI].includes(this.chainId) || (tokenSymbol as AvailableTokenSymbols) === 'eth')
return '0';
// Notify user about error if incorrect token symbol provided
if (!Object.values(InstanceTokenSymbol).includes(tokenSymbol)) {
console.error(
`Invalid token symbol: ${tokenSymbol}, must be lowercase token from one of Tornado ETH Mainnet pools`,
);
return '0';
}
// In Tornado we need to calculate refund only on user side, relayer get refund value in proof // 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]; const gasLimit = defaultInstanceTokensGasLimit[tokenSymbol];
return BigNumber.from(gasPrice).mul(gasLimit).mul(2).toHexString(); 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<HexadecimalStringifiedNumber>} Refund amount in WEI (in hex format)
*/
async fetchRefundInETH(tokenSymbol: InstanceTokenSymbol): Promise<HexadecimalStringifiedNumber> {
const gasPrice = await this.getGasPrice();
return this.calculateRefundInETH(gasPrice, tokenSymbol);
}
/** /**
* Get refund amount on ETH or Goerli in non-native token * 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 {HexadecimalStringifiedNumber | number} tokenDecimals Token (currency) decimals
* @param {InstanceTokenSymbol} tokenSymbol Withdrawal token (currency) symbol - for example, 'dai' * @param {InstanceTokenSymbol} tokenSymbol Withdrawal token (currency) symbol - for example, 'dai'
* @returns {Promise<HexadecimalStringifiedNumber>} 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, tokenPriceInEth: BigNumberish,
tokenDecimals: HexadecimalStringifiedNumber | number, tokenDecimals: HexadecimalStringifiedNumber | number,
tokenSymbol: InstanceTokenSymbol, tokenSymbol: InstanceTokenSymbol,
): Promise<HexadecimalStringifiedNumber> { ): HexadecimalStringifiedNumber {
const refundInEth = await this.calculateRefundInETH(tokenSymbol); const refundInEth = this.calculateRefundInETH(gasPrice, tokenSymbol);
return convertETHToToken(refundInEth, tokenDecimals, tokenPriceInEth).toHexString(); 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, * 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, * 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 * 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 {GetWithdrawalFeeViaRelayerInput} params Function input arguments object
* @param {TransactionData} tx Transaction data (object in web3 / ethers format) * @param {TxType} params.txType Tornado transaction type: withdrawal costs calculation from user side or from relayer side
* @param {number} relayerFeePercent Relayer fee percent from the transaction amount (for example, 0.15 for BNB or 0.4 for ETH Mainnet) * @param {TransactionData} [params.tx] Transaction data (object in web3 / ethers format)
* @param {AvailableTokenSymbols | Uppercase<AvailableTokenSymbols>} currency Currency symbol * @param {number} params.relayerFeePercent Relayer fee percent from the transaction amount (for example, 0.15 for BNB or 0.4 for ETH Mainnet)
* @param {number | HexadecimalStringifiedNumber } amount Withdrawal amount in selected currency * @param {AvailableTokenSymbols | Uppercase<AvailableTokenSymbols>} params.currency Currency symbol
* @param {number | HexadecimalStringifiedNumber } decimals Token (currency) decimals * @param {number | HexadecimalStringifiedNumber } params.amount Withdrawal amount in selected currency
* @param {BigNumberish} [refundInEth=0] Refund in ETH, if withdrawed other tokens on Mainnet (not ETH) * @param {number | HexadecimalStringifiedNumber } params.decimals Token (currency) decimals
* @param {BigNumberish} [tokenPriceInEth] If withdrawing other token on Mainnet or Goerli, need to provide token price in ETH (in WEI) * @param {BigNumberish} [params.refundInEth] Refund in ETH, if withdrawed other tokens on Mainnet (not ETH). Can not be provided, if user-side calculation
* @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<HexadecimalStringifiedNumber>} Fee in WEI (hexed string) * @returns {Promise<HexadecimalStringifiedNumber>} Fee in WEI (hexed string)
*/ */
async calculateWithdrawalFeeViaRelayer( async calculateWithdrawalFeeViaRelayer({
type: TxType, tx,
tx: TransactionData, txType,
relayerFeePercent: number, relayerFeePercent,
currency: AvailableTokenSymbols | Uppercase<AvailableTokenSymbols>, currency,
amount: HexadecimalStringifiedNumber | number, amount,
decimals: HexadecimalStringifiedNumber | number, decimals,
refundInEth: BigNumberish = 0, refundInEth,
tokenPriceInEth?: BigNumberish, tokenPriceInEth,
): Promise<HexadecimalStringifiedNumber> { predefinedGasLimit,
const gasCosts = BigNumber.from(await this.getGas(tx, type)); predefinedGasPrice,
}: GetWithdrawalFeeViaRelayerInput): Promise<HexadecimalStringifiedNumber> {
const relayerFee = parseUnits(amount.toString(), decimals) const relayerFee = this.calculateRelayerFeeInWei(relayerFeePercent, amount, decimals);
.mul(`${Math.floor(relayerFeePercent * 1e10)}`) const { gasPrice, gasLimit } = await this.getGasParams({ tx, txType, predefinedGasLimit, predefinedGasPrice });
.div(`${100 * 1e10}`); const gasCosts = BigNumber.from(gasPrice).mul(gasLimit);
if ((this.chainId === ChainId.MAINNET || this.chainId === ChainId.GOERLI) && currency.toLowerCase() != 'eth') { if ((this.chainId === ChainId.MAINNET || this.chainId === ChainId.GOERLI) && currency.toLowerCase() != 'eth') {
if (!tokenPriceInEth) { if (!tokenPriceInEth) {
@ -200,10 +267,13 @@ export abstract class TornadoFeeOracle implements ITornadoFeeOracle {
return '0'; return '0';
} }
const feeInEth = gasCosts.add(refundInEth); if (txType === 'user_withdrawal' && refundInEth === undefined)
refundInEth = this.calculateRefundInETH(gasPrice, currency.toLowerCase() as InstanceTokenSymbol);
const feeInEth = BigNumber.from(gasCosts).add(refundInEth || 0);
return convertETHToToken(feeInEth, decimals, tokenPriceInEth).add(relayerFee).toHexString(); return convertETHToToken(feeInEth, decimals, tokenPriceInEth).add(relayerFee).toHexString();
} }
return gasCosts.add(relayerFee).toHexString(); return BigNumber.from(gasCosts).add(relayerFee).toHexString();
} }
} }

View File

@ -4,6 +4,9 @@ import { ITornadoFeeOracle, TransactionData, TxType, LegacyGasPrices } from './t
import { GasPriceOracle } from '@tornado/gas-price-oracle'; import { GasPriceOracle } from '@tornado/gas-price-oracle';
import { bump } from './utils'; import { bump } from './utils';
/**
* Oracle for V4 (old-version) transactions - estimates fee with predefined gas limit and without smart bumping
*/
export class TornadoFeeOracleV4 extends TornadoFeeOracle implements ITornadoFeeOracle { export class TornadoFeeOracleV4 extends TornadoFeeOracle implements ITornadoFeeOracle {
public constructor(chainId: number, rpcUrl: string, defaultGasPrices?: LegacyGasPrices) { public constructor(chainId: number, rpcUrl: string, defaultGasPrices?: LegacyGasPrices) {
const oracleConfig = { const oracleConfig = {
@ -13,7 +16,7 @@ export class TornadoFeeOracleV4 extends TornadoFeeOracle implements ITornadoFeeO
}; };
const gasPriceOracle = new GasPriceOracle(oracleConfig); const gasPriceOracle = new GasPriceOracle(oracleConfig);
super(chainId, rpcUrl, gasPriceOracle); super(4, chainId, rpcUrl, gasPriceOracle);
} }
async getGasLimit(tx?: TransactionData, type: TxType = 'other', bumpPercent: number = 0): Promise<number> { async getGasLimit(tx?: TransactionData, type: TxType = 'other', bumpPercent: number = 0): Promise<number> {
@ -26,7 +29,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 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 // 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 (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(); return bump(await this.provider.estimateGas(tx), bumpPercent).toNumber();
} }

View File

@ -12,6 +12,9 @@ import { GasPriceOracle } from '@tornado/gas-price-oracle';
import { bump } from './utils'; import { bump } from './utils';
import { TornadoFeeOracleV4 } from './feeOracleV4'; import { TornadoFeeOracleV4 } from './feeOracleV4';
/**
* Oracle for new V5 version - estimates transaction fees with smart gas limit & bumping
*/
export class TornadoFeeOracleV5 extends TornadoFeeOracle implements ITornadoFeeOracle { export class TornadoFeeOracleV5 extends TornadoFeeOracle implements ITornadoFeeOracle {
private fallbackFeeOracle: TornadoFeeOracleV4; private fallbackFeeOracle: TornadoFeeOracleV4;
@ -26,7 +29,7 @@ export class TornadoFeeOracleV5 extends TornadoFeeOracle implements ITornadoFeeO
}; };
const gasPriceOracle = new GasPriceOracle(oracleConfig); const gasPriceOracle = new GasPriceOracle(oracleConfig);
super(chainId, rpcUrl, gasPriceOracle); super(5, chainId, rpcUrl, gasPriceOracle);
this.fallbackFeeOracle = new TornadoFeeOracleV4(chainId, rpcUrl, defaultGasPrices); this.fallbackFeeOracle = new TornadoFeeOracleV4(chainId, rpcUrl, defaultGasPrices);
} }

View File

@ -40,9 +40,10 @@ export class TokenPriceOracle implements ITornadoPriceOracle {
private provider: Provider; private provider: Provider;
/** /**
* * Constructs TokenPriceOracle class instance
* @param rpcUrl * @param {string} rpcUrl http RPC (Ethereum Mainnet) url to fetch token prices from contract
* @param tokens * @param {Token[]} [tokens] Array of tokens
* @param {TokenPrices} [defaultTokenPrices] Default token prices, fallback if nothing loaded from contract
*/ */
constructor( constructor(
rpcUrl: string, rpcUrl: string,
@ -61,7 +62,7 @@ export class TokenPriceOracle implements ITornadoPriceOracle {
/** /**
* Prepare data for MultiCall contract * Prepare data for MultiCall contract
* @param {Token[]} tokens Tokens array * @param {Token[]} [tokens] Tokens array
* @returns Valid structure to provide to MultiCall contract * @returns Valid structure to provide to MultiCall contract
*/ */
private prepareCallData(tokens: Token[] = this.tokens): MultiCall.CallStruct[] { private prepareCallData(tokens: Token[] = this.tokens): MultiCall.CallStruct[] {
@ -73,7 +74,7 @@ export class TokenPriceOracle implements ITornadoPriceOracle {
/** /**
* Fetch actual tokens price rate to ETH from offchain oracles * Fetch actual tokens price rate to ETH from offchain oracles
* @param {Token[]} tokens Token array * @param {Token[]} [tokens] Token array
* @returns {TokenPrices} Object with token price rate to ETH in WEI * @returns {TokenPrices} Object with token price rate to ETH in WEI
*/ */
async fetchPrices(tokens: Token[] = this.tokens): Promise<TokenPrices> { async fetchPrices(tokens: Token[] = this.tokens): Promise<TokenPrices> {

View File

@ -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 - '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 - '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 { export interface TransactionData {
to: string; to: string;
@ -35,40 +36,21 @@ export interface TransactionData {
} }
export interface ITornadoFeeOracle { export interface ITornadoFeeOracle {
getGasParams: ( getGasParams: (params?: GetGasParamsInput) => Promise<GetGasParamsRes>;
tx?: TransactionData, getGas: (params?: GetGasInput) => Promise<HexadecimalStringifiedNumber>;
txType?: TxType,
bumpGasLimitPercent?: number,
bumpGasPricePercent?: number,
speed?: LegacyGasPriceKey,
) => Promise<GetGasParamsRes>;
getGas: (
tx?: TransactionData,
type?: TxType,
bumpGasLimitPercent?: number,
bumpGasPricePercent?: number,
speed?: LegacyGasPriceKey,
) => Promise<HexadecimalStringifiedNumber>;
getGasPriceParams: (speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<GasPriceParams>; getGasPriceParams: (speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<GasPriceParams>;
getGasPrice: (speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<HexadecimalStringifiedNumber>; getGasPrice: (speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<HexadecimalStringifiedNumber>;
getGasLimit: (tx?: TransactionData, type?: TxType, bumpPercent?: number) => Promise<number>; getGasLimit: (tx?: TransactionData, type?: TxType, bumpPercent?: number) => Promise<number>;
fetchL1OptimismFee: (tx?: TransactionData) => Promise<HexadecimalStringifiedNumber>; fetchL1OptimismFee: (tx?: TransactionData) => Promise<HexadecimalStringifiedNumber>;
calculateRefundInETH: (tokenSymbol: InstanceTokenSymbol) => Promise<HexadecimalStringifiedNumber>; calculateRefundInETH: (gasPrice: BigNumberish, tokenSymbol: InstanceTokenSymbol) => HexadecimalStringifiedNumber;
fetchRefundInETH: (tokenSymbol: InstanceTokenSymbol) => Promise<HexadecimalStringifiedNumber>;
calculateRefundInToken: ( calculateRefundInToken: (
gasPrice: BigNumberish,
tokenPriceInEth: BigNumberish, tokenPriceInEth: BigNumberish,
tokenDecimals: HexadecimalStringifiedNumber | number, tokenDecimals: HexadecimalStringifiedNumber | number,
tokenSymbol: InstanceTokenSymbol, tokenSymbol: InstanceTokenSymbol,
) => Promise<HexadecimalStringifiedNumber>; ) => HexadecimalStringifiedNumber;
calculateWithdrawalFeeViaRelayer: ( calculateWithdrawalFeeViaRelayer: (params: GetWithdrawalFeeViaRelayerInput) => Promise<HexadecimalStringifiedNumber>;
type: TxType,
tx: TransactionData,
relayerFeePercent: number,
currency: AvailableTokenSymbols,
amount: HexadecimalStringifiedNumber | number,
decimals: number,
refundInEth: BigNumberish,
tokenPriceInEth?: BigNumberish,
) => Promise<HexadecimalStringifiedNumber>;
} }
export interface ITornadoPriceOracle { export interface ITornadoPriceOracle {
@ -99,6 +81,34 @@ export type TokenPrices = { [tokenSymbol in TokenSymbol]?: BigNumberish };
// Reponse type for getGasParams function of fee oracle // Reponse type for getGasParams function of fee oracle
export type GetGasParamsRes = { export type GetGasParamsRes = {
gasLimit: number; gasLimit: number;
gasPrice: HexadecimalStringifiedNumber; gasPrice: HexadecimalStringifiedNumber; // Gas price in native currency
l1Fee: HexadecimalStringifiedNumber; };
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<AvailableTokenSymbols>; // 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
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)
}; };

View File

@ -380,7 +380,7 @@
"@tornado/tornado-config@^2.0.0": "@tornado/tornado-config@^2.0.0":
version "2.0.0" version "2.0.0"
resolved "https://git.tornado.is/api/packages/tornado-packages/npm/%40tornado%2Ftornado-config/-/2.0.0/tornado-config-2.0.0.tgz#81b67bb946326ae8b1d359e8829d2f23ca6aeb2d" resolved "https://git.tornado.is/api/packages/tornado-packages/npm/%40tornado%2Ftornado-config/-/2.0.0/tornado-config-2.0.0.tgz"
integrity sha512-XckaC3A214du4UWXqBCB75NpqYGX8qL7tkTL9ttS+IQKpjOhqehmClOJGl51JXQGlQlgNSg8l2E+GUWt1kHaBw== integrity sha512-XckaC3A214du4UWXqBCB75NpqYGX8qL7tkTL9ttS+IQKpjOhqehmClOJGl51JXQGlQlgNSg8l2E+GUWt1kHaBw==
"@tsconfig/node10@^1.0.7": "@tsconfig/node10@^1.0.7":