Compare commits
34 Commits
b7769d4548
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9def6cd536 | |||
| 65af25e76b | |||
| 2ce8cafdbd | |||
| 394d6cab5c | |||
| 9ef88948d2 | |||
| be7f911a17 | |||
| ae9b15b7ed | |||
| 0d3ea02015 | |||
| 90471e0073 | |||
| c3bfb381b7 | |||
| 342c34b07a | |||
| 57c24e21eb | |||
| 583f3e90dd | |||
| fb772fa300 | |||
| 42b57f8734 | |||
| a0476b0e34 | |||
| de369f405c | |||
| 4cdd5c7538 | |||
| 76a0ccea68 | |||
| 40bf17177b | |||
| 3c1352ea41 | |||
| 1ace796f8a | |||
| 0aa15627f4 | |||
| 96616d8cf4 | |||
| 0d1ac74b11 | |||
| 2e3273b7bc | |||
| c63e6a320f | |||
| f5244ae722 | |||
| 03e6af5614 | |||
| 85b057fbed | |||
| 6bff5b560f | |||
| 4a1edeaef9 | |||
| c50caf3a48 | |||
| 2da3bf094f |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -90,4 +90,5 @@ sw.*
|
|||||||
# Vim swap files
|
# Vim swap files
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
test
|
test
|
||||||
|
TODO
|
||||||
@@ -1 +1,8 @@
|
|||||||
lib
|
abis
|
||||||
|
lib
|
||||||
|
README.md
|
||||||
|
src/contracts
|
||||||
|
node_modules
|
||||||
|
.prettierrc
|
||||||
|
tsconfig.*
|
||||||
|
package.json
|
||||||
82
README.md
Normal file
82
README.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Tornado oracles
|
||||||
|
|
||||||
|
This is a library providing convenient and fast access to oracles for Tornado-specific transactions, for example, withdrawal via relayer or getting rate to ETH for tokens that used in Tornado pools
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
- 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`
|
||||||
|
|
||||||
|
### Import
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { TornadoFeeOracleV5, TornadoFeeOracleV5, TokenPriceOracle } = require('@tornado/tornado-oracles');
|
||||||
|
or
|
||||||
|
import { TornadoFeeOracleV5, TornadoFeeOracleV5, TokenPriceOracle } from '@tornado/tornado-oracles';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
##### Estimate withdrawal gas costs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TornadoFeeOracleV5 } from '@tornado/tornado-oracles';
|
||||||
|
|
||||||
|
const tx: TransactionData = {
|
||||||
|
to: tornadoProxyLightAddress,
|
||||||
|
data: poolInstance.methods.withdraw(...params).encodeABI(),
|
||||||
|
value: withdrawalProofArgs[5],
|
||||||
|
};
|
||||||
|
|
||||||
|
const feeOracle = new TornadoFeeOracleV5(1, 'https://eth.llamarpc.com'); // First parameter - chain ID
|
||||||
|
const withdrawalGas = await feeOracle.getGas({tx, txType: 'relayer_withdrawal'});
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Estimate gas price and gas limit to send transaction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TornadoFeeOracleV5 } from '@tornado/tornado-oracles';
|
||||||
|
|
||||||
|
const incompleteTx: TransactionData = {
|
||||||
|
to: tornadoProxyLightAddress,
|
||||||
|
data: poolInstance.methods.withdraw(...params).encodeABI(),
|
||||||
|
value: withdrawalProofArgs[5],
|
||||||
|
};
|
||||||
|
|
||||||
|
const feeOracle = new TornadoFeeOracleV5(1, 'https://eth.llamarpc.com');
|
||||||
|
const transactionType: TxType = 'relayer_withdrawal';
|
||||||
|
const { gasPrice, gasLimit } = await feeOracle.getGasParams({tx: incompleteTx, txType: transactionType});
|
||||||
|
|
||||||
|
const tx: TransactionData = {...incompleteTx, gasPrice, gasLimit}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Get token prices (rate to ETH) for tokens that used in Tornado
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TokenPriceOracle } from '@tornado/tornado-oracles';
|
||||||
|
|
||||||
|
const priceOracle = new TokenPriceOracle('https://eth.llamarpc.com');
|
||||||
|
const tokenPrices = await priceOracle.fetchPrices();
|
||||||
|
|
||||||
|
console.log(tokenPrices); // All prices in WEI
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
torn: '1653773547906175',
|
||||||
|
dai: '603108348359886',
|
||||||
|
cdai: '13487984643748',
|
||||||
|
usdc: '601311723569085',
|
||||||
|
usdt: '602058974373161',
|
||||||
|
wbtc: '15696224089898846959'
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
17
package.json
17
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@tornado/tornado-oracles",
|
"name": "@tornado/tornado-oracles",
|
||||||
"version": "0.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,11 +11,12 @@
|
|||||||
"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",
|
||||||
"url": "https://git.tornado.ws/tornado-packages/tornado-gas-oracle.git"
|
"url": "https://git.tornado.ws/tornado-packages/tornado-oracles.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Gas",
|
"Gas",
|
||||||
@@ -30,16 +31,22 @@
|
|||||||
"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",
|
||||||
"typechain": "^8.3.1",
|
"typechain": "^8.3.1",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib/**/*"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://git.tornado.ws/api/packages/tornado-packages/npm"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ export enum ChainId {
|
|||||||
AVAX = 43114,
|
AVAX = 43114,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum InstanceTokenSymbol {
|
||||||
|
DAI = 'dai',
|
||||||
|
cDAI = 'cdai',
|
||||||
|
WBTC = 'wbtc',
|
||||||
|
USDT = 'usdt',
|
||||||
|
USDC = 'usdc',
|
||||||
|
}
|
||||||
|
|
||||||
export type GasPricesConfig = {
|
export type GasPricesConfig = {
|
||||||
[chainId in ChainId]: LegacyGasPrices;
|
[chainId in ChainId]: LegacyGasPrices;
|
||||||
};
|
};
|
||||||
@@ -81,6 +89,18 @@ export const defaultWithdrawalGasLimit: GasLimitConfig = {
|
|||||||
[ChainId.XDAI]: 390000,
|
[ChainId.XDAI]: 390000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type InstanceTokenGasLimitConfig = {
|
||||||
|
[tokenSymbol in InstanceTokenSymbol]: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultInstanceTokensGasLimit: InstanceTokenGasLimitConfig = {
|
||||||
|
[InstanceTokenSymbol.DAI]: 55000,
|
||||||
|
[InstanceTokenSymbol.cDAI]: 425000,
|
||||||
|
[InstanceTokenSymbol.WBTC]: 85000,
|
||||||
|
[InstanceTokenSymbol.USDT]: 100000,
|
||||||
|
[InstanceTokenSymbol.USDC]: 80000,
|
||||||
|
};
|
||||||
|
|
||||||
export const optimismL1FeeOracleAddress = '0x420000000000000000000000000000000000000F';
|
export const optimismL1FeeOracleAddress = '0x420000000000000000000000000000000000000F';
|
||||||
export const offchainOracleAddress = '0x07D91f5fb9Bf7798734C3f606dB065549F6893bb';
|
export const offchainOracleAddress = '0x07D91f5fb9Bf7798734C3f606dB065549F6893bb';
|
||||||
export const multiCallAddress = '0xda3c19c6fe954576707fa24695efb830d9cca1ca';
|
export const multiCallAddress = '0xda3c19c6fe954576707fa24695efb830d9cca1ca';
|
||||||
|
|||||||
288
src/feeOracle.ts
288
src/feeOracle.ts
@@ -1,17 +1,30 @@
|
|||||||
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 { TransactionData, TxType, ITornadoFeeOracle, GasPrice, LegacyGasPriceKey } from './types';
|
import {
|
||||||
import { Provider } from '@ethersproject/abstract-provider';
|
TransactionData,
|
||||||
import { ChainId, defaultGasPrices } from './config';
|
TxType,
|
||||||
import { bump, calculateGasPriceInWei, fromGweiToWeiHex, serializeTx } from './utils';
|
ITornadoFeeOracle,
|
||||||
|
LegacyGasPriceKey,
|
||||||
|
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 { 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: Provider;
|
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,
|
||||||
@@ -22,66 +35,123 @@ 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 Fee in WEI (MATIC)
|
* @returns {Promise<HexadecimalStringifiedNumber>} Fee in WEI (MATIC), '0' if chain is not Optimism
|
||||||
*/
|
*/
|
||||||
private async fetchL1OptimismFee(tx?: TransactionData): Promise<BigNumber> {
|
async fetchL1OptimismFee(tx?: TransactionData): Promise<HexadecimalStringifiedNumber> {
|
||||||
if (this.chainId != ChainId.OPTIMISM) return BigNumber.from(0);
|
if (this.chainId != ChainId.OPTIMISM) return BigNumber.from(0).toHexString();
|
||||||
|
|
||||||
const optimismL1FeeOracle = getOptimismL1FeeOracle(this.provider);
|
const optimismL1FeeOracle = getOptimismL1FeeOracle(this.provider);
|
||||||
|
const l1Fee = await optimismL1FeeOracle.getL1Fee(serializeTx(tx));
|
||||||
|
|
||||||
return await optimismL1FeeOracle.getL1Fee(serializeTx(tx));
|
return l1Fee.toHexString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate gas price, gas limit and l1Fee for sidechain (if exists)
|
||||||
|
* @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<GetGasParamsRes>} Object with fields 'gasPrice' and 'gasLimit', L1 fee, if exists, included in gasLimit
|
||||||
|
*/
|
||||||
|
async getGasParams(params: GetGasParamsInput = {}): Promise<GetGasParamsRes> {
|
||||||
|
let {
|
||||||
|
tx,
|
||||||
|
txType = 'other',
|
||||||
|
bumpGasLimitPercent,
|
||||||
|
bumpGasPricePercent,
|
||||||
|
predefinedGasLimit: gasLimit,
|
||||||
|
predefinedGasPrice: gasPrice,
|
||||||
|
speed,
|
||||||
|
includeL1FeeToGasLimit = true,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
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} type Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other'
|
* @param {TransactionData} [params.tx] Transaction data in web3 / ethers format
|
||||||
* @param {LegacyGasPriceKey} speed Preferred transaction speed, if uses legacy gas (before EIP-1559)
|
* @param {TxType} [params.txType] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other'
|
||||||
* @returns {Promise<string>} Gas value in WEI (hex-format)
|
* @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<HexadecimalStringifiedNumber>} Gas value in WEI (hex-format)
|
||||||
*/
|
*/
|
||||||
public async getGas(
|
async getGas(params: GetGasInput = {}): Promise<HexadecimalStringifiedNumber> {
|
||||||
tx?: TransactionData,
|
const { gasPrice, gasLimit } = await this.getGasParams({ ...params, includeL1FeeToGasLimit: true });
|
||||||
type: TxType = 'other',
|
|
||||||
speed: LegacyGasPriceKey = 'fast',
|
|
||||||
): Promise<BigNumber> {
|
|
||||||
const gasPrice = await this.getGasPrice(type, speed);
|
|
||||||
let gas = BigNumber.from(0);
|
|
||||||
gas = gas.add(calculateGasPriceInWei(gasPrice));
|
|
||||||
if (tx) tx = Object.assign(tx, { gasPrice });
|
|
||||||
gas = gas.mul(await this.getGasLimit(tx, type));
|
|
||||||
if (this.chainId === ChainId.OPTIMISM) gas = gas.add(await this.fetchL1OptimismFee(tx));
|
|
||||||
|
|
||||||
return gas;
|
return BigNumber.from(gasPrice).mul(gasLimit).toHexString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate next block gas price
|
* Estimate next block gas price
|
||||||
* @param {TxType} type Tornado transaction type (to select correct default bump percent)
|
* @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=0] Gas bump percent to prioritize transaction
|
||||||
* @param {number} bumpPercent Gas bump percent to prioritize transaction
|
* @returns {Promise<GasPriceParams>} Estimated gas price info in WEI (hexed) - legacy object with gasPrice property or
|
||||||
* @returns {GasPrice} Estimated gas price info in WEI (hexed) - legacy object with gasPrice property or EIP-1559 object with maxFeePerGas
|
* EIP-1559 object with maxFeePerGas and maxPriorityFeePerGas properties
|
||||||
* and maxPriorityFeePerGas properties
|
|
||||||
*/
|
*/
|
||||||
async getGasPrice(
|
async getGasPriceParams(speed?: LegacyGasPriceKey, bumpPercent: number = 0): Promise<GasPriceParams> {
|
||||||
type: TxType = 'other',
|
// Use instant for BSC, because on this chain "fast" and "instant" differs more than third and "fast" transaction can take hours
|
||||||
speed: LegacyGasPriceKey = 'fast',
|
if (!speed) speed = this.chainId === ChainId.BSC ? 'instant' : 'fast';
|
||||||
bumpPercent: number = 0,
|
|
||||||
): Promise<GasPrice> {
|
|
||||||
try {
|
try {
|
||||||
return await this.oracle.getTxGasParams({ legacySpeed: speed, bumpPercent });
|
return await this.oracle.getTxGasParams({ legacySpeed: speed, bumpPercent });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { gasPrice: bump(fromGweiToWeiHex(defaultGasPrices[this.chainId][speed]), bumpPercent) };
|
return { gasPrice: bump(fromGweiToWeiHex(defaultGasPrices[this.chainId][speed]), bumpPercent).toHexString() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimates gas limit for transaction (or basic gas limit, if no tx data provided)
|
* Estimate next block gas price
|
||||||
* @param {TransactionData} tx Transaction data (object in web3 / ethers format)
|
* @param {LegacyGasPriceKey} [speed] Preferred transaction speed, if uses legacy gas (before EIP-1559)
|
||||||
* @param {TxType} type Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other'
|
* @param {number} [bumpPercent] Gas bump percent to prioritize transaction
|
||||||
* @returns {Promise<BigNumber>} Gas limit
|
* @returns {Promise<HexadecimalStringifiedNumber>} Gas price in WEI (hex string)
|
||||||
*/
|
*/
|
||||||
abstract getGasLimit(tx?: TransactionData, type?: TxType): Promise<BigNumber>;
|
async getGasPrice(speed?: LegacyGasPriceKey, bumpPercent?: number): Promise<HexadecimalStringifiedNumber> {
|
||||||
|
const gasPriceParams = await this.getGasPriceParams(speed, bumpPercent);
|
||||||
|
return calculateGasPriceInWei(gasPriceParams).toHexString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {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
|
||||||
|
* @returns {Promise<number>} Gas limit
|
||||||
|
*/
|
||||||
|
abstract getGasLimit(tx?: TransactionData, type?: TxType, bumpPercent?: number): Promise<number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If user withdraw non-native tokens on ETH or Goerli, we need to calculate refund value:
|
* If user withdraw non-native tokens on ETH or Goerli, we need to calculate refund value:
|
||||||
@@ -89,53 +159,121 @@ 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 {BigNumberish} gasPrice Actual gas price
|
||||||
|
* @param {InstanceTokenSymbol} tokenSymbol Withdrawal token (currency) symbol - for example, 'dai'
|
||||||
|
* @returns {HexadecimalStringifiedNumber} Refund amount in WEI (in hex format)
|
||||||
*/
|
*/
|
||||||
async calculateRefundInETH(): Promise<BigNumber> {
|
calculateRefundInETH(gasPrice: BigNumberish, tokenSymbol: InstanceTokenSymbol): HexadecimalStringifiedNumber {
|
||||||
return (await this.getGas()).mul(2);
|
// 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
|
||||||
|
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<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
|
||||||
|
* @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 {HexadecimalStringifiedNumber} Refund amount in WEI in selected token (hexed number)
|
||||||
|
*/
|
||||||
|
calculateRefundInToken(
|
||||||
|
gasPrice: BigNumberish,
|
||||||
|
tokenPriceInEth: BigNumberish,
|
||||||
|
tokenDecimals: HexadecimalStringifiedNumber | number,
|
||||||
|
tokenSymbol: InstanceTokenSymbol,
|
||||||
|
): 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,
|
* 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 {string} 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} amount Withdrawal amount in selected currency
|
* @param {AvailableTokenSymbols | Uppercase<AvailableTokenSymbols>} params.currency Currency symbol
|
||||||
* @param {number} decimals Token (currency) decimals
|
* @param {number | HexadecimalStringifiedNumber } params.amount Withdrawal amount in selected currency
|
||||||
* @param {BigNumberish} refund 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
|
||||||
* @returns {Promise<BigNumber>} Fee in WEI
|
* @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)
|
||||||
*/
|
*/
|
||||||
async calculateWithdrawalFeeViaRelayer(
|
async calculateWithdrawalFeeViaRelayer({
|
||||||
type: TxType,
|
tx,
|
||||||
tx: TransactionData,
|
txType,
|
||||||
relayerFeePercent: number,
|
relayerFeePercent,
|
||||||
currency: AvailableTokenSymbols,
|
currency,
|
||||||
amount: string,
|
amount,
|
||||||
decimals: number,
|
decimals,
|
||||||
refund: BigNumberish = 0,
|
refundInEth,
|
||||||
tokenPriceInEth?: BigNumberish,
|
tokenPriceInEth,
|
||||||
): Promise<BigNumber> {
|
predefinedGasLimit,
|
||||||
let withdrawalFee = BigNumber.from(0);
|
predefinedGasPrice,
|
||||||
const gasCosts = await this.getGas(tx, type);
|
}: GetWithdrawalFeeViaRelayerInput): Promise<HexadecimalStringifiedNumber> {
|
||||||
withdrawalFee = gasCosts;
|
const relayerFee = this.calculateRelayerFeeInWei(relayerFeePercent, amount, decimals);
|
||||||
|
const { gasPrice, gasLimit } = await this.getGasParams({ tx, txType, predefinedGasLimit, predefinedGasPrice });
|
||||||
const relayerFee = parseUnits(amount, decimals)
|
const gasCosts = BigNumber.from(gasPrice).mul(gasLimit);
|
||||||
.mul(`${relayerFeePercent * 1e10}`)
|
|
||||||
.div(`${100 * 1e10}`);
|
|
||||||
|
|
||||||
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) {
|
||||||
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 '0';
|
||||||
}
|
}
|
||||||
const tokenDecimals = BigNumber.from(10).pow(decimals);
|
|
||||||
return withdrawalFee.add(refund).mul(tokenDecimals).div(tokenPriceInEth).add(relayerFee);
|
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 withdrawalFee.add(relayerFee);
|
return BigNumber.from(gasCosts).add(relayerFee).toHexString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { defaultWithdrawalGasLimit } from './config';
|
|||||||
import { TornadoFeeOracle } from './feeOracle';
|
import { TornadoFeeOracle } from './feeOracle';
|
||||||
import { ITornadoFeeOracle, TransactionData, TxType, LegacyGasPrices } from './types';
|
import { ITornadoFeeOracle, TransactionData, TxType, LegacyGasPrices } from './types';
|
||||||
import { GasPriceOracle } from '@tornado/gas-price-oracle';
|
import { GasPriceOracle } from '@tornado/gas-price-oracle';
|
||||||
import { BigNumber } from 'ethers';
|
|
||||||
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 = {
|
||||||
@@ -14,17 +16,21 @@ 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'): Promise<BigNumber> {
|
async getGasLimit(tx?: TransactionData, type: TxType = 'other', bumpPercent: number = 0): Promise<number> {
|
||||||
if (type === 'user_withdrawal') return BigNumber.from(defaultWithdrawalGasLimit[this.chainId]);
|
if (type === 'user_withdrawal') return bump(defaultWithdrawalGasLimit[this.chainId], bumpPercent).toNumber();
|
||||||
|
|
||||||
// Need to bump relayer gas limit for transaction, because predefined gas limit to small to be 100% sure that transaction will be sent
|
// Need to bump relayer gas limit for transaction, because predefined gas limit to small to be 100% sure that transaction will be sent
|
||||||
// This leads to fact that relayer often pays extra for gas from his own funds, however, this was designed by previous developers
|
// This leads to fact that relayer often pays extra for gas from his own funds, however, this was designed by previous developers
|
||||||
if (type === 'relayer_withdrawal') return bump(defaultWithdrawalGasLimit[this.chainId], 20);
|
if (type === 'relayer_withdrawal')
|
||||||
if (!tx) return BigNumber.from(21_000);
|
return bump(defaultWithdrawalGasLimit[this.chainId], bumpPercent || 25).toNumber();
|
||||||
|
// 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(23_000, bumpPercent).toNumber();
|
||||||
|
|
||||||
return this.provider.estimateGas(tx);
|
return bump(await this.provider.estimateGas(tx), bumpPercent).toNumber();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,41 @@
|
|||||||
import { ChainId, defaultWithdrawalGasLimit } from './config';
|
import { ChainId } from './config';
|
||||||
import { TornadoFeeOracle } from './feeOracle';
|
import { TornadoFeeOracle } from './feeOracle';
|
||||||
import { ITornadoFeeOracle, GasPrice, TransactionData, TxType, LegacyGasPrices, LegacyGasPriceKey } from './types';
|
import {
|
||||||
|
ITornadoFeeOracle,
|
||||||
|
TransactionData,
|
||||||
|
TxType,
|
||||||
|
LegacyGasPrices,
|
||||||
|
LegacyGasPriceKey,
|
||||||
|
GasPriceParams,
|
||||||
|
} from './types';
|
||||||
import { GasPriceOracle } from '@tornado/gas-price-oracle';
|
import { GasPriceOracle } from '@tornado/gas-price-oracle';
|
||||||
import { BigNumber } from 'ethers';
|
|
||||||
import { bump } from './utils';
|
import { bump } from './utils';
|
||||||
|
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;
|
||||||
|
|
||||||
public constructor(chainId: number, rpcUrl: string, defaultGasPrices?: LegacyGasPrices) {
|
public constructor(chainId: number, rpcUrl: string, defaultGasPrices?: LegacyGasPrices) {
|
||||||
const oracleConfig = {
|
const oracleConfig = {
|
||||||
chainId,
|
chainId,
|
||||||
defaultRpc: rpcUrl,
|
defaultRpc: rpcUrl,
|
||||||
minPriority: chainId === 1 || chainId === 5 ? 2 : 0.05,
|
minPriority: chainId === ChainId.MAINNET || chainId === ChainId.GOERLI ? 2 : chainId === ChainId.BSC ? 3 : 0.05,
|
||||||
percentile: 5,
|
percentile: 5,
|
||||||
blocksCount: 20,
|
blocksCount: 20,
|
||||||
defaultFallbackGasPrices: defaultGasPrices,
|
defaultFallbackGasPrices: defaultGasPrices,
|
||||||
};
|
};
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGasLimit(tx?: TransactionData, type: TxType = 'other', bumpPercent: number = 20): Promise<BigNumber> {
|
async getGasLimit(tx?: TransactionData, type: TxType = 'other', bumpPercent: number = 20): Promise<number> {
|
||||||
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
|
||||||
@@ -32,23 +46,18 @@ export class TornadoFeeOracleV5 extends TornadoFeeOracle implements ITornadoFeeO
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const fetchedGasLimit = await this.provider.estimateGas(tx);
|
const fetchedGasLimit = await this.provider.estimateGas(tx);
|
||||||
return bump(fetchedGasLimit, bumpPercent);
|
return bump(fetchedGasLimit, bumpPercent).toNumber();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (type.endsWith('withdrawal')) return bump(defaultWithdrawalGasLimit[this.chainId], bumpPercent);
|
return this.fallbackFeeOracle.getGasLimit(tx, type, bumpPercent);
|
||||||
return BigNumber.from(21_000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGasPrice(
|
async getGasPriceParams(speed?: LegacyGasPriceKey, bumpPercent?: number): Promise<GasPriceParams> {
|
||||||
type: TxType = 'other',
|
|
||||||
speed: LegacyGasPriceKey = 'fast',
|
|
||||||
bumpPercent?: number,
|
|
||||||
): Promise<GasPrice> {
|
|
||||||
// Only if bump percent didn't provided (if user provides 0, no need to recalculate)
|
// Only if bump percent didn't provided (if user provides 0, no need to recalculate)
|
||||||
if (bumpPercent === undefined) {
|
if (bumpPercent === undefined) {
|
||||||
switch (this.chainId) {
|
switch (this.chainId) {
|
||||||
case ChainId.GOERLI:
|
case ChainId.GOERLI:
|
||||||
bumpPercent = type === 'user_withdrawal' ? 100 : 50;
|
bumpPercent = 100;
|
||||||
break;
|
break;
|
||||||
case ChainId.POLYGON:
|
case ChainId.POLYGON:
|
||||||
case ChainId.AVAX:
|
case ChainId.AVAX:
|
||||||
@@ -60,6 +69,6 @@ export class TornadoFeeOracleV5 extends TornadoFeeOracle implements ITornadoFeeO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.getGasPrice(type, speed, bumpPercent);
|
return super.getGasPriceParams(speed, bumpPercent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { bump } from './utils';
|
|
||||||
|
|
||||||
export * from './feeOracleV4';
|
export * from './feeOracleV4';
|
||||||
export * from './feeOracleV5';
|
export * from './feeOracleV5';
|
||||||
export * from './tokenPriceOracle';
|
export * from './tokenPriceOracle';
|
||||||
export { bump };
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MultiCall } from './contracts/MulticallAbi';
|
import { MultiCall } from './contracts/MulticallAbi';
|
||||||
import { Token, TokenPrices, TokenSymbol } from './types';
|
import { ITornadoPriceOracle, Token, TokenPrices, TokenSymbol } from './types';
|
||||||
import { MulticallAbi, OffchainOracleAbi } from './contracts';
|
import { MulticallAbi, OffchainOracleAbi } from './contracts';
|
||||||
import { getMultiCallContract, getOffchainOracleContract } from './contracts/factories';
|
import { getMultiCallContract, getOffchainOracleContract } from './contracts/factories';
|
||||||
import { Provider } from '@ethersproject/abstract-provider';
|
import { Provider } from '@ethersproject/abstract-provider';
|
||||||
@@ -25,40 +25,47 @@ const tornToken: Token = {
|
|||||||
};
|
};
|
||||||
const tornadoTokens = [tornToken, ...filterTokensFromTornadoInstances(instances)];
|
const tornadoTokens = [tornToken, ...filterTokensFromTornadoInstances(instances)];
|
||||||
|
|
||||||
const defaultTokenPrices: TokenPrices = {
|
const defaultTornadoTokenPrices: TokenPrices = {
|
||||||
cdai: '12143621157112',
|
torn: '1689423546359032',
|
||||||
dai: '543174175301531',
|
dai: '598416104472725',
|
||||||
usdc: '543496781058130',
|
cdai: '13384388487019',
|
||||||
usdt: '540358243246849',
|
usdc: '599013776676721',
|
||||||
wbtc: '15944433090979762571',
|
usdt: '599323410893614',
|
||||||
torn: '1683813509941878',
|
wbtc: '15659889148334216720',
|
||||||
};
|
};
|
||||||
|
|
||||||
export class TokenPriceOracle {
|
export class TokenPriceOracle implements ITornadoPriceOracle {
|
||||||
private oracle: OffchainOracleAbi;
|
private oracle: OffchainOracleAbi;
|
||||||
private multiCall: MulticallAbi;
|
private multiCall: MulticallAbi;
|
||||||
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,
|
||||||
private tokens: Token[] = tornadoTokens,
|
private tokens: Token[] = tornadoTokens,
|
||||||
|
private defaultTokenPrices: TokenPrices = defaultTornadoTokenPrices,
|
||||||
) {
|
) {
|
||||||
this.provider = new ethers.providers.JsonRpcProvider(rpcUrl);
|
this.provider = new ethers.providers.JsonRpcProvider(rpcUrl);
|
||||||
this.oracle = getOffchainOracleContract(this.provider);
|
this.oracle = getOffchainOracleContract(this.provider);
|
||||||
this.multiCall = getMultiCallContract(this.provider);
|
this.multiCall = getMultiCallContract(this.provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instant return default token prices
|
||||||
|
get defaultPrices(): TokenPrices {
|
||||||
|
return this.defaultTokenPrices;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
prepareCallData(tokens: Token[] = this.tokens): MultiCall.CallStruct[] {
|
private prepareCallData(tokens: Token[] = this.tokens): MultiCall.CallStruct[] {
|
||||||
return tokens.map((token) => ({
|
return tokens.map((token) => ({
|
||||||
to: this.oracle.address,
|
to: this.oracle.address,
|
||||||
data: this.oracle.interface.encodeFunctionData('getRateToEth', [token.tokenAddress, true]),
|
data: this.oracle.interface.encodeFunctionData('getRateToEth', [token.tokenAddress, true]),
|
||||||
@@ -67,7 +74,7 @@ export class TokenPriceOracle {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 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> {
|
||||||
@@ -81,7 +88,7 @@ export class TokenPriceOracle {
|
|||||||
for (let i = 0; i < results.length; i++) {
|
for (let i = 0; i < results.length; i++) {
|
||||||
const tokenSymbol = tokens[i].symbol.toLowerCase() as TokenSymbol;
|
const tokenSymbol = tokens[i].symbol.toLowerCase() as TokenSymbol;
|
||||||
if (!success[i]) {
|
if (!success[i]) {
|
||||||
if (defaultTokenPrices[tokenSymbol]) prices[tokenSymbol] = defaultTokenPrices[tokenSymbol];
|
if (this.defaultTokenPrices[tokenSymbol]) prices[tokenSymbol] = this.defaultTokenPrices[tokenSymbol];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +101,7 @@ export class TokenPriceOracle {
|
|||||||
return prices;
|
return prices;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Cannot get token prices, return default: ' + e);
|
console.error('Cannot get token prices, return default: ' + e);
|
||||||
return defaultTokenPrices;
|
return this.defaultTokenPrices;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
src/types.ts
90
src/types.ts
@@ -1,15 +1,25 @@
|
|||||||
import { BigNumber, BigNumberish, BytesLike } from 'ethers';
|
import { BigNumberish, BytesLike } from 'ethers';
|
||||||
import { GasPriceKey } from '@tornado/gas-price-oracle/lib/services';
|
import { GasPriceKey, GetTxGasParamsRes } from '@tornado/gas-price-oracle/lib/services';
|
||||||
import { AvailableTokenSymbols } from '@tornado/tornado-config';
|
import { AvailableTokenSymbols } from '@tornado/tornado-config';
|
||||||
|
import { InstanceTokenSymbol } from './config';
|
||||||
|
|
||||||
|
// Type for big hexadecimal numbers, like 0x1eff87f47e37a0
|
||||||
|
export type HexadecimalStringifiedNumber = string;
|
||||||
|
|
||||||
export type LegacyGasPriceKey = GasPriceKey;
|
export type LegacyGasPriceKey = GasPriceKey;
|
||||||
// Gas Prices in WEI
|
export type GasPriceParams = GetTxGasParamsRes;
|
||||||
export type GasPrice = { maxFeePerGas: BigNumberish; maxPriorityFeePerGas: BigNumberish } | { gasPrice: BigNumberish };
|
|
||||||
export type LegacyGasPrices = {
|
export type LegacyGasPrices = {
|
||||||
[gasPriceType in LegacyGasPriceKey]: number;
|
[gasPriceType in LegacyGasPriceKey]: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TxType = 'user_withdrawal' | 'relayer_withdrawal' | 'other';
|
/* Tornado-specific transaction types:
|
||||||
|
- 'user_withdrawal' - Fee calculation on user side, when user withdraw funds itself or via relayer on site
|
||||||
|
- 'relayer_withdrawal' - Fee calculation on relayer side, when relayer sends withdrawal transaction
|
||||||
|
- '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 WithdrawalTxType = 'user_withdrawal' | 'relayer_withdrawal' | 'relayer_withdrawal_check_v4';
|
||||||
|
export type TxType = WithdrawalTxType | 'other';
|
||||||
|
|
||||||
export interface TransactionData {
|
export interface TransactionData {
|
||||||
to: string;
|
to: string;
|
||||||
@@ -26,20 +36,26 @@ export interface TransactionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ITornadoFeeOracle {
|
export interface ITornadoFeeOracle {
|
||||||
getGas: (tx?: TransactionData, type?: TxType) => Promise<BigNumber>;
|
getGasParams: (params?: GetGasParamsInput) => Promise<GetGasParamsRes>;
|
||||||
getGasPrice: (type?: TxType, speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<GasPrice>;
|
getGas: (params?: GetGasInput) => Promise<HexadecimalStringifiedNumber>;
|
||||||
getGasLimit: (tx?: TransactionData, type?: TxType, bumpPercent?: number) => Promise<BigNumber>;
|
getGasPriceParams: (speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<GasPriceParams>;
|
||||||
calculateRefundInETH: () => Promise<BigNumber>;
|
getGasPrice: (speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<HexadecimalStringifiedNumber>;
|
||||||
calculateWithdrawalFeeViaRelayer: (
|
getGasLimit: (tx?: TransactionData, type?: TxType, bumpPercent?: number) => Promise<number>;
|
||||||
type: TxType,
|
fetchL1OptimismFee: (tx?: TransactionData) => Promise<HexadecimalStringifiedNumber>;
|
||||||
tx: TransactionData,
|
calculateRefundInETH: (gasPrice: BigNumberish, tokenSymbol: InstanceTokenSymbol) => HexadecimalStringifiedNumber;
|
||||||
relayerFeePercent: number,
|
fetchRefundInETH: (tokenSymbol: InstanceTokenSymbol) => Promise<HexadecimalStringifiedNumber>;
|
||||||
currency: AvailableTokenSymbols,
|
calculateRefundInToken: (
|
||||||
amount: string,
|
gasPrice: BigNumberish,
|
||||||
decimals: number,
|
tokenPriceInEth: BigNumberish,
|
||||||
refund: BigNumberish,
|
tokenDecimals: HexadecimalStringifiedNumber | number,
|
||||||
tokenPriceInEth?: BigNumberish,
|
tokenSymbol: InstanceTokenSymbol,
|
||||||
) => Promise<BigNumber>;
|
) => HexadecimalStringifiedNumber;
|
||||||
|
calculateWithdrawalFeeViaRelayer: (params: GetWithdrawalFeeViaRelayerInput) => Promise<HexadecimalStringifiedNumber>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITornadoPriceOracle {
|
||||||
|
defaultPrices: TokenPrices;
|
||||||
|
fetchPrices: (tokens?: Token[]) => Promise<TokenPrices>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WithdrawalData = {
|
export type WithdrawalData = {
|
||||||
@@ -53,6 +69,7 @@ export type TornadoPoolInstance = {
|
|||||||
decimals: number;
|
decimals: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// All non-native tokens from Tornado instances on ETH mainnet and TORN itself
|
||||||
export type TokenSymbol = 'dai' | 'cdai' | 'usdc' | 'usdt' | 'wbtc' | 'torn';
|
export type TokenSymbol = 'dai' | 'cdai' | 'usdc' | 'usdt' | 'wbtc' | 'torn';
|
||||||
export type Token = {
|
export type Token = {
|
||||||
tokenAddress: string;
|
tokenAddress: string;
|
||||||
@@ -60,3 +77,38 @@ export type Token = {
|
|||||||
decimals: number;
|
decimals: number;
|
||||||
};
|
};
|
||||||
export type TokenPrices = { [tokenSymbol in TokenSymbol]?: BigNumberish };
|
export type TokenPrices = { [tokenSymbol in TokenSymbol]?: BigNumberish };
|
||||||
|
|
||||||
|
// Reponse type for getGasParams function of fee oracle
|
||||||
|
export type GetGasParamsRes = {
|
||||||
|
gasLimit: number;
|
||||||
|
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<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)
|
||||||
|
};
|
||||||
|
|||||||
16
src/utils.ts
16
src/utils.ts
@@ -1,5 +1,5 @@
|
|||||||
import { serialize } from '@ethersproject/transactions';
|
import { serialize } from '@ethersproject/transactions';
|
||||||
import { GasPrice, TransactionData } from './types';
|
import { GasPriceParams, TransactionData } from './types';
|
||||||
import { BigNumber, BigNumberish } from 'ethers';
|
import { BigNumber, BigNumberish } from 'ethers';
|
||||||
import BigNumberFloat from 'bignumber.js';
|
import BigNumberFloat from 'bignumber.js';
|
||||||
BigNumberFloat.config({ EXPONENTIAL_AT: 100 });
|
BigNumberFloat.config({ EXPONENTIAL_AT: 100 });
|
||||||
@@ -13,9 +13,9 @@ export function serializeTx(tx?: TransactionData | string): string {
|
|||||||
return serialize(tx);
|
return serialize(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateGasPriceInWei(gasPrice: GasPrice): BigNumber {
|
export function calculateGasPriceInWei(gasPrice: GasPriceParams): BigNumber {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return gasPrice.gasPrice || gasPrice.maxFeePerGas;
|
return BigNumber.from(gasPrice.gasPrice || gasPrice.maxFeePerGas);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bump(value: BigNumberish, percent: number): BigNumber {
|
export function bump(value: BigNumberish, percent: number): BigNumber {
|
||||||
@@ -25,6 +25,7 @@ export function bump(value: BigNumberish, percent: number): BigNumber {
|
|||||||
BigNumberFloat(BigNumber.from(value).toHexString())
|
BigNumberFloat(BigNumber.from(value).toHexString())
|
||||||
.times(hundredPercents.plus(BigNumberFloat(percent)))
|
.times(hundredPercents.plus(BigNumberFloat(percent)))
|
||||||
.div(hundredPercents)
|
.div(hundredPercents)
|
||||||
|
.decimalPlaces(0, 1)
|
||||||
.toString(),
|
.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -32,3 +33,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);
|
||||||
|
}
|
||||||
|
|||||||
14
yarn.lock
14
yarn.lock
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user