Compare commits

..

4 Commits

8 changed files with 93 additions and 58 deletions

2
.nvmrc

@ -1 +1 @@
v20.3.0 v14.21.3

@ -6,23 +6,19 @@ This is a library providing convenient and fast access to oracles for Tornado-sp
<hr> <hr>
* Create `.npmrc` file in project root with content `@tornado:registry=https://git.tornado.ws/api/packages/tornado-packages/npm/` - Create `.npmrc` file in project root with content `@tornado:registry=https://git.tornado.ws/api/packages/tornado-packages/npm/`
* Run `npm i @tornado/tornado-oracles` - Run `npm i @tornado/tornado-oracles`
### Import ### Import
<hr> <hr>
```typescript ```typescript
const {TornadoFeeOracleV5, TornadoFeeOracleV5, TokenPriceOracle} = require('@tornado/tornado-oracles') const { TornadoFeeOracleV5, TornadoFeeOracleV5, TokenPriceOracle } = require('@tornado/tornado-oracles');
or or
import {TornadoFeeOracleV5, TornadoFeeOracleV5, TokenPriceOracle} from '@tornado/tornado-oracles' import { TornadoFeeOracleV5, TornadoFeeOracleV5, TokenPriceOracle } from '@tornado/tornado-oracles';
``` ```
### Usage ### Usage
<hr> <hr>
@ -30,31 +26,31 @@ import {TornadoFeeOracleV5, TornadoFeeOracleV5, TokenPriceOracle} from '@tornado
##### Estimate withdrawal gas costs ##### Estimate withdrawal gas costs
```typescript ```typescript
import { TornadoFeeOracleV5 } from '@tornado/tornado-oracles' import { TornadoFeeOracleV5 } from '@tornado/tornado-oracles';
const tx: TransactionData = { const tx: TransactionData = {
to: tornadoProxyLightAddress, to: tornadoProxyLightAddress,
data: poolInstance.methods.withdraw(...params).encodeABI(), data: poolInstance.methods.withdraw(...params).encodeABI(),
value: withdrawalProofArgs[5] value: withdrawalProofArgs[5],
} };
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, 'relayer_withdrawal');
``` ```
##### Estimate gas price and gas limit to send transaction ##### Estimate gas price and gas limit to send transaction
```typescript ```typescript
import { TornadoFeeOracleV5 } from '@tornado/tornado-oracles' import { TornadoFeeOracleV5 } from '@tornado/tornado-oracles';
const incompleteTx: TransactionData = { const incompleteTx: TransactionData = {
to: tornadoProxyLightAddress, to: tornadoProxyLightAddress,
data: poolInstance.methods.withdraw(...params).encodeABI(), data: poolInstance.methods.withdraw(...params).encodeABI(),
value: withdrawalProofArgs[5] value: withdrawalProofArgs[5],
} };
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.getGasPriceInHex(transactionType); const gasPrice = await feeOracle.getGasPriceInHex(transactionType);
const gasLimit = await feeOracle.getGasLimit(incompleteTx, transactionType); const gasLimit = await feeOracle.getGasLimit(incompleteTx, transactionType);
@ -64,9 +60,9 @@ const tx: TransactionData = Object.assign({ gasPrice, gasLimit }, incompleteTx);
##### Get token prices (rate to ETH) for tokens that used in Tornado ##### Get token prices (rate to ETH) for tokens that used in Tornado
```typescript ```typescript
import { TokenPriceOracle } from '@tornado/tornado-oracles' import { TokenPriceOracle } from '@tornado/tornado-oracles';
const priceOracle = new TokenPriceOracle("https://eth.llamarpc.com"); const priceOracle = new TokenPriceOracle('https://eth.llamarpc.com');
const tokenPrices = await priceOracle.fetchPrices(); const tokenPrices = await priceOracle.fetchPrices();
console.log(tokenPrices); // All prices in WEI console.log(tokenPrices); // All prices in WEI
@ -84,4 +80,4 @@ console.log(tokenPrices); // All prices in WEI
### License ### License
[MIT](LICENSE) [MIT](LICENSE)

@ -1,6 +1,6 @@
{ {
"name": "@tornado/tornado-oracles", "name": "@tornado/tornado-oracles",
"version": "0.2.3", "version": "0.3.0",
"description": "Gas oracle for Tornado-specific transactions", "description": "Gas oracle for Tornado-specific transactions",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
@ -30,12 +30,12 @@
"dependencies": { "dependencies": {
"@tornado/gas-price-oracle": "^0.5.3", "@tornado/gas-price-oracle": "^0.5.3",
"@tornado/tornado-config": "^2.0.0", "@tornado/tornado-config": "^2.0.0",
"@types/node": "^20.5.1",
"bignumber.js": "^9.1.1", "bignumber.js": "^9.1.1",
"ethers": "5.7" "ethers": "5.7"
}, },
"devDependencies": { "devDependencies": {
"@typechain/ethers-v5": "^11.1.1", "@typechain/ethers-v5": "^11.1.1",
"@types/node": "^20.4.10",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"prettier": "^3.0.1", "prettier": "^3.0.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",

@ -2,14 +2,14 @@ import { GasPriceOracle } from '@tornado/gas-price-oracle';
import { BigNumber, BigNumberish, ethers } from 'ethers'; import { BigNumber, BigNumberish, ethers } from 'ethers';
import { parseUnits } from 'ethers/lib/utils'; import { parseUnits } from 'ethers/lib/utils';
import { TransactionData, TxType, ITornadoFeeOracle, GasPrice, LegacyGasPriceKey } from './types'; import { TransactionData, TxType, ITornadoFeeOracle, GasPrice, LegacyGasPriceKey } from './types';
import { Provider } from '@ethersproject/abstract-provider'; import { JsonRpcProvider } from '@ethersproject/providers';
import { ChainId, defaultGasPrices } from './config'; import { ChainId, defaultGasPrices } from './config';
import { bump, calculateGasPriceInWei, fromGweiToWeiHex, serializeTx } from './utils'; import { bump, calculateGasPriceInWei, convertETHToToken, fromGweiToWeiHex, serializeTx } from './utils';
import { getOptimismL1FeeOracle } from './contracts/factories'; import { getOptimismL1FeeOracle } from './contracts/factories';
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: Provider; protected provider: JsonRpcProvider;
public constructor( public constructor(
protected chainId: ChainId, protected chainId: ChainId,
@ -94,7 +94,7 @@ 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 or 'other' * @param {TxType} type Tornado transaction type: withdrawal by user, withdrawal by relayer, relayer fee check or 'other'
* @returns {Promise<BigNumber>} Gas limit * @returns {Promise<BigNumber>} Gas limit
*/ */
abstract getGasLimit(tx?: TransactionData, type?: TxType): Promise<BigNumber>; abstract getGasLimit(tx?: TransactionData, type?: TxType): Promise<BigNumber>;
@ -105,10 +105,30 @@ 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)
* @returns {Promise<BigNumber>} Refund amount * @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'
* @returns {Promise<BigNumber>} Refund amount in WEI
*/ */
async calculateRefundInETH(): Promise<BigNumber> { async calculateRefundInETH(tx?: TransactionData, type?: TxType): Promise<BigNumber> {
return (await this.getGas()).mul(2); return (await this.getGas(tx, type)).mul(2);
}
/**
* Get refund amount on ETH or Goerli in non-native token
* @param {BigNumberish} tokenPriceInEth Token price in WEI in native currency
* @param {string | number} tokenDecimals Token (currency) decimals
* @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'
* @returns {Promise<BigNumber>} Refund amount in WEI in selected token
*/
async calculateRefundInToken(
tokenPriceInEth: BigNumberish,
tokenDecimals: string | number,
tx?: TransactionData,
type?: TxType,
): Promise<BigNumber> {
const refundInEth = await this.calculateRefundInETH(tx, type);
return convertETHToToken(refundInEth, tokenDecimals, tokenPriceInEth);
} }
/** /**
@ -118,28 +138,26 @@ export abstract class TornadoFeeOracle implements ITornadoFeeOracle {
* @param {TxType} type Tornado transaction type: withdrawal costs calculation from user side or from relayer side * @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 {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 {number} relayerFeePercent Relayer fee percent from the transaction amount (for example, 0.15 for BNB or 0.4 for ETH Mainnet)
* @param {string} currency Currency symbol * @param {AvailableTokenSymbols | Uppercase<AvailableTokenSymbols>} currency Currency symbol
* @param {number} amount Withdrawal amount in selected currency * @param {number | string } amount Withdrawal amount in selected currency
* @param {number} decimals Token (currency) decimals * @param {number | string } decimals Token (currency) decimals
* @param {BigNumberish} refund Refund in ETH, if withdrawed other tokens on Mainnet (not ETH) * @param {BigNumberish} [refund=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 {BigNumberish} [tokenPriceInEth] If withdrawing other token on Mainnet or Goerli, need to provide token price in ETH (in WEI)
* @returns {Promise<BigNumber>} Fee in WEI * @returns {Promise<BigNumber>} Fee in WEI
*/ */
async calculateWithdrawalFeeViaRelayer( async calculateWithdrawalFeeViaRelayer(
type: TxType, type: TxType,
tx: TransactionData, tx: TransactionData,
relayerFeePercent: number, relayerFeePercent: number,
currency: AvailableTokenSymbols, currency: AvailableTokenSymbols | Uppercase<AvailableTokenSymbols>,
amount: string, amount: string | number,
decimals: number, decimals: string | number,
refund: BigNumberish = 0, refund: BigNumberish = 0,
tokenPriceInEth?: BigNumberish, tokenPriceInEth?: BigNumberish,
): Promise<BigNumber> { ): Promise<BigNumber> {
let withdrawalFee = BigNumber.from(0);
const gasCosts = await this.getGas(tx, type); const gasCosts = await this.getGas(tx, type);
withdrawalFee = gasCosts;
const relayerFee = parseUnits(amount, decimals) const relayerFee = parseUnits(amount.toString(), decimals)
.mul(`${relayerFeePercent * 1e10}`) .mul(`${relayerFeePercent * 1e10}`)
.div(`${100 * 1e10}`); .div(`${100 * 1e10}`);
@ -148,10 +166,11 @@ export abstract class TornadoFeeOracle implements ITornadoFeeOracle {
console.error('Token price is required argument, if not native chain token is withdrawed'); console.error('Token price is required argument, if not native chain token is withdrawed');
return BigNumber.from(0); return BigNumber.from(0);
} }
const tokenDecimals = BigNumber.from(10).pow(decimals);
return withdrawalFee.add(refund).mul(tokenDecimals).div(tokenPriceInEth).add(relayerFee); const feeInEth = gasCosts.add(refund);
return convertETHToToken(feeInEth, decimals, tokenPriceInEth).add(relayerFee);
} }
return withdrawalFee.add(relayerFee); return gasCosts.add(relayerFee);
} }
} }

@ -4,8 +4,11 @@ import { ITornadoFeeOracle, GasPrice, TransactionData, TxType, LegacyGasPrices,
import { GasPriceOracle } from '@tornado/gas-price-oracle'; import { GasPriceOracle } from '@tornado/gas-price-oracle';
import { BigNumber } from 'ethers'; import { BigNumber } from 'ethers';
import { bump } from './utils'; import { bump } from './utils';
import { TornadoFeeOracleV4 } from './feeOracleV4';
export class TornadoFeeOracleV5 extends TornadoFeeOracle implements ITornadoFeeOracle { export class TornadoFeeOracleV5 extends TornadoFeeOracle implements ITornadoFeeOracle {
private fallbackFeeOracle: TornadoFeeOracleV4;
public constructor(chainId: number, rpcUrl: string, defaultGasPrices?: LegacyGasPrices) { public constructor(chainId: number, rpcUrl: string, defaultGasPrices?: LegacyGasPrices) {
const oracleConfig = { const oracleConfig = {
chainId, chainId,
@ -18,10 +21,12 @@ export class TornadoFeeOracleV5 extends TornadoFeeOracle implements ITornadoFeeO
const gasPriceOracle = new GasPriceOracle(oracleConfig); const gasPriceOracle = new GasPriceOracle(oracleConfig);
super(chainId, rpcUrl, gasPriceOracle); super(chainId, rpcUrl, gasPriceOracle);
this.fallbackFeeOracle = new TornadoFeeOracleV4(chainId, rpcUrl, defaultGasPrices);
} }
async getGasLimit(tx?: TransactionData, type: TxType = 'other', bumpPercent: number = 20): Promise<BigNumber> { async getGasLimit(tx?: TransactionData, type: TxType = 'other', bumpPercent: number = 20): Promise<BigNumber> {
if (!tx) return BigNumber.from(21_000); if (!tx || Object.keys(tx).length === 0) return this.fallbackFeeOracle.getGasLimit(tx, type, bumpPercent);
/* Relayer gas limit must be lower so that fluctuations in gas price cannot lead to the fact that /* Relayer gas limit must be lower so that fluctuations in gas price cannot lead to the fact that
* the relayer will actually pay for gas more than the money allocated for this by the user * the relayer will actually pay for gas more than the money allocated for this by the user

@ -36,7 +36,13 @@ export interface ITornadoFeeOracle {
getGasPrice: (type?: TxType, speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<GasPrice>; getGasPrice: (type?: TxType, speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<GasPrice>;
getGasPriceInHex: (type?: TxType, speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<string>; getGasPriceInHex: (type?: TxType, speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<string>;
getGasLimit: (tx?: TransactionData, type?: TxType, bumpPercent?: number) => Promise<BigNumber>; getGasLimit: (tx?: TransactionData, type?: TxType, bumpPercent?: number) => Promise<BigNumber>;
calculateRefundInETH: () => Promise<BigNumber>; calculateRefundInETH: (tx?: TransactionData, type?: TxType) => Promise<BigNumber>;
calculateRefundInToken: (
tokenPriceInEth: BigNumberish,
tokenDecimals: string | number,
tx?: TransactionData,
type?: TxType,
) => Promise<BigNumber>;
calculateWithdrawalFeeViaRelayer: ( calculateWithdrawalFeeViaRelayer: (
type: TxType, type: TxType,
tx: TransactionData, tx: TransactionData,

@ -32,3 +32,12 @@ export function bump(value: BigNumberish, percent: number): BigNumber {
export function fromGweiToWeiHex(value: number | string): BigNumberish { export function fromGweiToWeiHex(value: number | string): BigNumberish {
return BigNumber.from(BigNumberFloat(value).times(GWEI).toString()).toHexString(); return BigNumber.from(BigNumberFloat(value).times(GWEI).toString()).toHexString();
} }
export function convertETHToToken(
amountInWEI: BigNumberish,
tokenDecimals: number | string,
tokenPriceInWei: BigNumberish,
): BigNumber {
const tokenDecimalsMultiplier = BigNumber.from(10).pow(tokenDecimals);
return BigNumber.from(amountInWEI).mul(tokenDecimalsMultiplier).div(tokenPriceInWei);
}

@ -411,10 +411,10 @@
lodash "^4.17.15" lodash "^4.17.15"
ts-essentials "^7.0.1" ts-essentials "^7.0.1"
"@types/node@^20.4.10": "@types/node@^20.5.1":
version "20.4.10" version "20.5.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.10.tgz#73c9480791e3ddeb4887a660fc93a7f59353ad45" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30"
integrity sha512-vwzFiiy8Rn6E0MtA13/Cxxgpan/N6UeNYR9oUu6kuJWxu6zCk98trcDp8CBhbtaeuq9SykCmXkFr2lWLoPcvLg== integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==
"@types/prettier@^2.1.1": "@types/prettier@^2.1.1":
version "2.7.3" version "2.7.3"
@ -959,9 +959,9 @@ prettier@^2.3.1:
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
prettier@^3.0.1: prettier@^3.0.1:
version "3.0.1" version "3.0.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.1.tgz#65271fc9320ce4913c57747a70ce635b30beaa40" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b"
integrity sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ== integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==
pstree.remy@^1.1.8: pstree.remy@^1.1.8:
version "1.1.8" version "1.1.8"