diff --git a/CHANGELOG.md b/CHANGELOG.md index 827b0ceb..f1c7c690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.15.0] - 2024-FEBRUARY-21 + +- Improved the accuracy of `estimateGasFee` function by incorporating L1 rollup fee for destination L2 chain. + +Breaking Changes: + +The `sourceChainTokenSymbol` will not be required anymore. If not specified, the native token of source chain will be used to calculate estimated fee. The following functions are affected: + +- `calculateNativeGasFee` +- `estimateGasFee` + ## [0.14.2] - 2024-MARCH-5 - Add configs for L2s on testnet; add configs for Blast, Fraxtal on mainnet diff --git a/package.json b/package.json index 0bf1afa4..e9f00413 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "bech32": "^2.0.0", "clone-deep": "^4.0.1", "cross-fetch": "^3.1.5", + "ethereum-multicall": "^2.21.0", "ethers": "^5.7.2", "socket.io-client": "^4.6.1", "standard-http-error": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99829e85..0b4a2736 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ dependencies: cross-fetch: specifier: ^3.1.5 version: 3.1.5 + ethereum-multicall: + specifier: ^2.21.0 + version: 2.21.0 ethers: specifier: ^5.7.2 version: 5.7.2 @@ -3563,6 +3566,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /ethereum-multicall@2.21.0: + resolution: {integrity: sha512-J234OuvUheTKvZVhMk41SwyB66m+MU+Xe2FFWOln8xu6TXKzOzsjSFQn/f5OTDGEiRStKMnJpCvQDim+Uk+qBQ==} + dependencies: + '@ethersproject/providers': 5.7.2 + ethers: 5.7.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /ethers@5.7.2: resolution: {integrity: sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==} dependencies: @@ -4533,6 +4546,7 @@ packages: /node-gyp-build@4.6.0: resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} hasBin: true + requiresBuild: true dev: true /node-releases@2.0.12: diff --git a/src/libs/AxelarQueryAPI.ts b/src/libs/AxelarQueryAPI.ts index 2a5edae7..8717fa81 100644 --- a/src/libs/AxelarQueryAPI.ts +++ b/src/libs/AxelarQueryAPI.ts @@ -3,7 +3,13 @@ import { parseUnits } from "ethers/lib/utils"; import { loadAssets } from "../assets"; import { EnvironmentConfigs, getConfigs } from "../constants"; import { RestService } from "../services"; -import { AxelarQueryAPIConfig, BaseFeeResponse, Environment } from "./types"; +import { + AxelarQueryAPIConfig, + BaseFeeResponse, + Environment, + EstimateL1FeeParams, + FeeToken, +} from "./types"; import { EvmChain } from "../constants/EvmChain"; import { GasToken } from "../constants/GasToken"; import { AxelarQueryClient, AxelarQueryClientType } from "./AxelarQueryClient"; @@ -16,9 +22,12 @@ import { import { throwIfInvalidChainIds } from "../utils"; import { loadChains } from "../chains"; import s3 from "./TransactionRecoveryApi/constants/s3"; -import { BigNumber, BigNumberish } from "ethers"; +import { BigNumber, BigNumberish, ethers } from "ethers"; import { ChainInfo } from "src/chains/types"; import { BigNumberUtils } from "./BigNumberUtils"; +import { rpcMap as testnetRpcMap } from "./TransactionRecoveryApi/constants/chain/testnet"; +import { rpcMap as mainnetRpcMap } from "./TransactionRecoveryApi/constants/chain/mainnet"; +import { getL1FeeForL2 } from "./fee/getL1Fee"; interface TranslatedTransferRateLimitResponse { incoming: string; @@ -38,6 +47,8 @@ export interface AxelarQueryAPIFeeResponse { baseFee: string; executionFee: string; executionFeeWithMultiplier: string; + l1ExecutionFeeWithMultiplier: string; + l1ExecutionFee: string; gasMultiplier: number; gasLimit: BigNumberish; minGasPrice: string; @@ -215,9 +226,11 @@ export class AxelarQueryAPI { const { source_base_fee_string, source_token, + ethereum_token, destination_native_token, express_fee_string, express_supported, + l2_type, } = response.result; const execute_gas_multiplier = response.result.execute_gas_multiplier as number; const { decimals: sourceTokenDecimals } = source_token; @@ -229,13 +242,18 @@ export class AxelarQueryAPI { return { baseFee, expressFee, - sourceToken: source_token, + sourceToken: source_token as BaseFeeResponse["sourceToken"], executeGasMultiplier: parseFloat(execute_gas_multiplier.toFixed(2)), destToken: { gas_price: destination_native_token.gas_price, - gas_price_gwei: parseInt(destination_native_token.gas_price_gwei).toString(), decimals: destination_native_token.decimals, + token_price: destination_native_token.token_price, + name: destination_native_token.name, + symbol: destination_native_token.symbol, + l1_gas_price_in_units: destination_native_token.l1_gas_price_in_units, }, + l2_type, + ethereumToken: ethereum_token as BaseFeeResponse["ethereumToken"], apiResponse: response, success: true, expressSupported: express_supported, @@ -243,59 +261,120 @@ export class AxelarQueryAPI { }); } + public async estimateL1GasFee(destChainId: EvmChain | string, l1FeeParams: EstimateL1FeeParams) { + // Retrieve the RPC URL for the source chain to calculate L1 fee + const rpcMap = this.environment === "mainnet" ? mainnetRpcMap : testnetRpcMap; + + // Throw an error if the RPC URL is not found + if (!rpcMap[destChainId]) { + throw new Error(`RPC URL not found for chain ${destChainId}`); + } + + const provider = new ethers.providers.JsonRpcProvider(rpcMap[destChainId]); + + return getL1FeeForL2(provider, l1FeeParams); + } + + public async calculateL1FeeForDestL2( + destChainId: EvmChain | string, + destToken: FeeToken, + executeData: `0x${string}` | undefined, + sourceToken: FeeToken, + ethereumToken: BaseFeeResponse["ethereumToken"], + actualGasMultiplier: number, + l2Type: BaseFeeResponse["l2_type"] + ): Promise<[BigNumber, BigNumber]> { + let l1ExecutionFee = BigNumber.from(0); + let l1ExecutionFeeWithMultiplier = BigNumber.from(0); + + if (destToken.l1_gas_price_in_units) { + if (!executeData) { + console.warn( + `Since you did not provide executeData, this API will not accurately calculate the + total required fee as we will not be able to capture the L1 inclusion fee for this L2 chain.` + ); + } + + // Calculate the L1 execution fee. This value is in ETH. + l1ExecutionFee = await this.estimateL1GasFee(destChainId, { + executeData: executeData || "0x", + l1GasPrice: destToken.l1_gas_price_in_units, + l2Type, + }); + + // Convert the L1 execution fee to the source token + const srcTokenPrice = Number(sourceToken.token_price.usd); + const ethTokenPrice = Number(ethereumToken.token_price.usd); + const ethToSrcTokenPriceRatio = ethTokenPrice / srcTokenPrice; + + const actualL1ExecutionFee = Math.ceil(l1ExecutionFee.toNumber() * ethToSrcTokenPriceRatio); + + l1ExecutionFee = BigNumber.from(actualL1ExecutionFee.toString()); + + // Calculate the L1 execution fee with the gas multiplier + l1ExecutionFeeWithMultiplier = BigNumber.from( + Math.floor(actualGasMultiplier * actualGasMultiplier) + ); + } + + return [l1ExecutionFee, l1ExecutionFeeWithMultiplier]; + } + /** * Calculate estimated gas amount to pay for the gas receiver contract. * @param sourceChainId Can be of the EvmChain enum or string. If string, should try to generalize to use the CHAINS constants (e.g. CHAINS.MAINNET.ETHEREUM) * @param destinationChainId Can be of the EvmChain enum or string. If string, should try to generalize to use the CHAINS constants (e.g. CHAINS.MAINNET.ETHEREUM) - * @param sourceChainTokenSymbol * @param gasLimit An estimated gas amount required to execute `executeWithToken` function. * @param gasMultiplier (Optional) A multiplier used to create a buffer above the calculated gas fee, to account for potential slippage throughout tx execution, e.g. 1.1 = 10% buffer. supports up to 3 decimal places * The default value is "auto", which uses the gas multiplier from the fee response + * @param sourceChainTokenSymbol (Optional) the gas token symbol on the source chain. * @param minGasPrice (Optional) A minimum value, in wei, for the gas price on the destination chain that is used to override the estimated gas price if it falls below this specified value. + * @param executeData (Optional) The data to be executed on the destination chain. It's recommended to specify it if the destination chain is an L2 chain to calculate more accurate gas fee. * @param gmpParams (Optional) Additional parameters for GMP transactions, including the ability to see a detailed view of the fee response * @returns */ public async estimateGasFee( sourceChainId: EvmChain | string, destinationChainId: EvmChain | string, - sourceChainTokenSymbol: GasToken | string, gasLimit: BigNumberish, gasMultiplier: number | "auto" = "auto", + sourceChainTokenSymbol?: GasToken | string, minGasPrice = "0", + executeData?: `0x${string}`, gmpParams?: GMPParams ): Promise { await throwIfInvalidChainIds([sourceChainId, destinationChainId], this.environment); - const response = await this.getNativeGasBaseFee( - sourceChainId, - destinationChainId, - sourceChainTokenSymbol as GasToken, - gmpParams?.tokenSymbol, - gmpParams?.destinationContractAddress, - gmpParams?.sourceContractAddress, - gmpParams?.transferAmount, - gmpParams?.transferAmountInUnits - ); + if (!BigNumber.from(gasLimit).gt(0)) { + throw new Error("Gas limit must be provided"); + } const { baseFee, expressFee, sourceToken, + ethereumToken, executeGasMultiplier, destToken, apiResponse, + l2_type, success, expressSupported, - } = response; + } = await this.getNativeGasBaseFee( + sourceChainId, + destinationChainId, + sourceChainTokenSymbol as GasToken, + gmpParams?.tokenSymbol, + gmpParams?.destinationContractAddress, + gmpParams?.sourceContractAddress, + gmpParams?.transferAmount, + gmpParams?.transferAmountInUnits + ); if (!success || !baseFee || !sourceToken) { throw new Error("Failed to estimate gas fee"); } - if (!BigNumber.from(gasLimit).gt(0)) { - throw new Error("Gas limit must be provided"); - } - const destGasFeeWei = BigNumberUtils.multiplyToGetWei( BigNumber.from(gasLimit), destToken.gas_price, @@ -320,19 +399,31 @@ export class AxelarQueryAPI { ? executionFee.mul(actualGasMultiplier * 10000).div(10000) : executionFee; + const [l1ExecutionFee, l1ExecutionFeeWithMultiplier] = await this.calculateL1FeeForDestL2( + destinationChainId, + destToken, + executeData, + sourceToken, + ethereumToken, + actualGasMultiplier, + l2_type + ); + return gmpParams?.showDetailedFees ? { baseFee, expressFee, - executionFee: executionFee.toString(), + executionFee: executionFeeWithMultiplier.toString(), executionFeeWithMultiplier: executionFeeWithMultiplier.toString(), + l1ExecutionFeeWithMultiplier: l1ExecutionFeeWithMultiplier.toString(), + l1ExecutionFee: l1ExecutionFee.toString(), gasLimit, gasMultiplier: actualGasMultiplier, minGasPrice: minGasPrice === "0" ? "NA" : minGasPrice, apiResponse, isExpressSupported: expressSupported, } - : executionFeeWithMultiplier.add(baseFee).toString(); + : l1ExecutionFeeWithMultiplier.add(executionFeeWithMultiplier).add(baseFee).toString(); } /** diff --git a/src/libs/TransactionRecoveryApi/AxelarGMPRecoveryAPI.ts b/src/libs/TransactionRecoveryApi/AxelarGMPRecoveryAPI.ts index b024da47..d32e12d4 100644 --- a/src/libs/TransactionRecoveryApi/AxelarGMPRecoveryAPI.ts +++ b/src/libs/TransactionRecoveryApi/AxelarGMPRecoveryAPI.ts @@ -11,7 +11,6 @@ import { Environment, } from "../types"; import { EvmChain } from "../../constants/EvmChain"; -import { GasToken } from "../../constants/GasToken"; import { AxelarRecoveryApi, ExecuteParams, @@ -21,7 +20,6 @@ import { import EVMClient from "./client/EVMClient"; import IAxelarExecutable from "../abi/IAxelarExecutable"; import { ContractReceipt, ContractTransaction, ethers } from "ethers"; -import { nativeGasTokenSymbol } from "../../constants"; import { AxelarQueryAPI } from "../AxelarQueryAPI"; import rpcInfo from "./constants/chain"; import { @@ -189,7 +187,6 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { return currentBlock - gmpTx.call.blockNumber; } - console.log(receipt); return receipt.confirmations; }); @@ -742,7 +739,6 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { txHash: string, sourceChain: string, destinationChain: string, - gasTokenSymbol: GasToken | string, estimatedGasUsed: number, options: QueryGasFeeOptions ): Promise { @@ -757,7 +753,6 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { return this.subtractGasFee( sourceChain, destinationChain, - gasTokenSymbol, paidGasFee, estimatedGasUsed, options @@ -777,7 +772,6 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { txHash: string, sourceChain: EvmChain, destinationChain: EvmChain, - gasTokenSymbol: GasToken | string, estimatedGasUsed: number, options: QueryGasFeeOptions ): Promise { @@ -790,7 +784,6 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { return this.subtractGasFee( sourceChain, destinationChain, - gasTokenSymbol, paidGasFee, estimatedGasUsed, options @@ -864,9 +857,9 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { amount: await this.axelarQueryApi.estimateGasFee( tx.call.chain, tx.call.returnValues.destinationChain, - tx.gas_paid?.returnValues?.denom ?? "uaxl", gasLimit, - autocalculateGasOptions?.gasMultipler + autocalculateGasOptions?.gasMultipler, + tx.gas_paid?.returnValues?.denom ?? "uaxl" ), }; @@ -938,7 +931,6 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { chain, "gas_service" ); - const gasToken = nativeGasTokenSymbol[this.environment][chain]; const receipt = await signer.provider.getTransactionReceipt(txHash); if (!receipt) return InvalidTransactionError(chain); @@ -960,7 +952,6 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { txHash, chain, destinationChain, - gasToken, estimatedGasUsed, { gasMultipler: options?.gasMultipler, @@ -1054,7 +1045,6 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { txHash, chain, destinationChain as EvmChain, - gasTokenSymbol, estimatedGasUsed, { provider: evmWalletDetails.provider, @@ -1161,7 +1151,6 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { private async subtractGasFee( sourceChain: string, destinationChain: string, - gasTokenSymbol: string, paidGasFee: string, estimatedGas: number, options: QueryGasFeeOptions @@ -1169,9 +1158,9 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { const totalGasFee = await this.axelarQueryApi.estimateGasFee( sourceChain, destinationChain, - gasTokenSymbol, estimatedGas, options.gasMultipler, + options.gasTokenSymbol, undefined, undefined ); @@ -1179,7 +1168,7 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi { let topupGasAmount = ethers.BigNumber.from(totalGasFee); if (options.shouldSubtractBaseFee) { const response = await this.axelarQueryApi - .getNativeGasBaseFee(sourceChain, destinationChain, gasTokenSymbol as GasToken) + .getNativeGasBaseFee(sourceChain, destinationChain) .catch(() => undefined); if (response && response.baseFee) { topupGasAmount = topupGasAmount.sub(response.baseFee); diff --git a/src/libs/TransactionRecoveryApi/constants/chain/mainnet.ts b/src/libs/TransactionRecoveryApi/constants/chain/mainnet.ts index a3b4d471..045181ff 100644 --- a/src/libs/TransactionRecoveryApi/constants/chain/mainnet.ts +++ b/src/libs/TransactionRecoveryApi/constants/chain/mainnet.ts @@ -1,12 +1,12 @@ import { EvmChain } from "../../../../constants/EvmChain"; import { Network } from "@ethersproject/networks"; -export const rpcMap: Partial> = { +export const rpcMap: Partial> = { [EvmChain.FANTOM]: "https://rpc.ftm.tools", [EvmChain.POLYGON]: "https://polygon-rpc.com", [EvmChain.MOONBEAM]: "https://rpc.api.moonbeam.network", [EvmChain.AVALANCHE]: "https://api.avax.network/ext/bc/C/rpc", - [EvmChain.ETHEREUM]: "https://mainnet.infura.io/v3/510b6d5b3c56497b8070626a54f565a9", + [EvmChain.ETHEREUM]: "https://ethereum.publicnode.com", [EvmChain.AURORA]: "https://mainnet.aurora.dev", [EvmChain.BINANCE]: "https://bsc-dataseed.binance.org", [EvmChain.BNBCHAIN]: "https://bsc-dataseed.binance.org", diff --git a/src/libs/fee/getL1Fee.spec.ts b/src/libs/fee/getL1Fee.spec.ts new file mode 100644 index 00000000..42c146ba --- /dev/null +++ b/src/libs/fee/getL1Fee.spec.ts @@ -0,0 +1,54 @@ +import { ethers } from "ethers"; +import { rpcMap } from "../TransactionRecoveryApi/constants/chain/mainnet"; +import { Environment, EstimateL1FeeParams } from "../types"; +import { getL1FeeForL2 } from "./getL1Fee"; +import { AxelarQueryAPI } from "../AxelarQueryAPI"; + +const MOCK_EXECUTE_DATA = + "0x1a98b2e0e68ba0eb84262d4bcf91955ec2680b37bcedd59a1f48e18d183dac9961bf9d1400000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000d40000000000000000000000000000000000000000000000000000000000deac2c6000000000000000000000000000000000000000000000000000000000000000762696e616e636500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a307863653136463639333735353230616230313337376365374238386635424138433438463844363636000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bc000000000000000000000000000000000000000000000000000000000000000400000000000000000000000004607cad6135d7a119185ebe062d3b369b1b536ef000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000580000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000009200000000000000000000000000000000000000000000000000000000000000a8000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc450000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc800000000000000000000000000000000000000000000000000000000000000640000000000000000000000004fd39c9e151e50580779bd04b1f7ecc310079fd3000000000000000000000000000000000000000000000000000000000deac2c6000000000000000000000000000000000000000000000000000000000dc647500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f40521500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc450000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc80000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc800000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000640000000000000000000000004fd39c9e151e50580779bd04b1f7ecc310079fd3000000000000000000000000000000000000000000000000000000000de83dbf000000000000000000000000000000000000000000000000015d8c7908dbe7130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc80000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004607cad6135d7a119185ebe062d3b369b1b536ef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000761786c5553444300000000000000000000000000000000000000000000000000"; + +describe("getL1Fee", () => { + const env = Environment.MAINNET; + + it("query optimism l1 fee should work", async () => { + const srcChain = "avalanche"; + const destChain = "optimism"; + + const queryAPI = new AxelarQueryAPI({ environment: env }); + + const { destToken, l2_type } = await queryAPI.getNativeGasBaseFee(srcChain, destChain); + + const provider = new ethers.providers.JsonRpcProvider(rpcMap[destChain]); + + const params: EstimateL1FeeParams = { + executeData: MOCK_EXECUTE_DATA, + l1GasPrice: destToken.l1_gas_price_in_units!, + l2Type: l2_type, + }; + + const fee = await getL1FeeForL2(provider, params); + + expect(fee).toBeDefined(); + }); + + it("query mantle l1 fee should work", async () => { + const srcChain = "avalanche"; + const destChain = "mantle"; + + const queryAPI = new AxelarQueryAPI({ environment: env }); + + const { destToken, l2_type } = await queryAPI.getNativeGasBaseFee(srcChain, destChain); + + const provider = new ethers.providers.JsonRpcProvider(rpcMap[destChain]); + + const params: EstimateL1FeeParams = { + executeData: MOCK_EXECUTE_DATA, + l1GasPrice: destToken.l1_gas_price_in_units!, + l2Type: l2_type, + }; + + const fee = await getL1FeeForL2(provider, params); + + expect(fee).toBeDefined(); + }); +}); diff --git a/src/libs/fee/getL1Fee.ts b/src/libs/fee/getL1Fee.ts new file mode 100644 index 00000000..f65a05f8 --- /dev/null +++ b/src/libs/fee/getL1Fee.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ + +import { BigNumber, ethers } from "ethers"; +import { EstimateL1FeeParams } from "../types"; +import { Multicall, ContractCallContext, ContractCallResults } from "ethereum-multicall"; + +const ABI = { + Optimism: [ + "function getL1GasUsed(bytes executeData) returns (uint256)", + "function scalar() returns (uint256)", + "function overhead() returns (uint256)", + ], + Mantle: ["function overhead() returns (uint256)", "function scalar() returns (uint256)"], +}; + +/** + * Get the estimated L1 fee for a given L2 chain. + * @param env The environment to use. Either "mainnet" or "testnet". + * @param chain The destination L2 chain. + * @param params The parameters to use for the estimation. + * @returns The estimated L1 fee. + */ +export function getL1FeeForL2( + provider: ethers.providers.JsonRpcProvider, + params: EstimateL1FeeParams +): Promise { + const multicall = new Multicall({ ethersProvider: provider, tryAggregate: true }); + + switch (params.l2Type) { + case "mantle": + return getMantleL1Fee(multicall, params); + case "op": + return getOptimismL1Fee(multicall, params); + // Most of the ethereum clients are already included L1 fee in the gas estimation for Arbitrum. + case "arb": + default: + return Promise.resolve(BigNumber.from(0)); + } +} + +async function getOptimismL1Fee(multicall: Multicall, estimateL1FeeParams: EstimateL1FeeParams) { + const { l1GasPrice, executeData } = estimateL1FeeParams; + const results = await multicall.call(buildContractCallContext("optimism", executeData)); + const { gasUsed, fixedOverhead, dynamicOverhead } = extractMulticallResults("optimism", results); + return calculateL1Fee(gasUsed, fixedOverhead, dynamicOverhead, BigNumber.from(l1GasPrice.value)); +} + +async function getMantleL1Fee(multicall: Multicall, estimateL1FeeParams: EstimateL1FeeParams) { + const { l1GasPrice, executeData } = estimateL1FeeParams; + const results = await multicall.call(buildContractCallContext("mantle", executeData)); + const { gasUsed, fixedOverhead, dynamicOverhead } = extractMulticallResults("mantle", results); + return calculateL1Fee(gasUsed, fixedOverhead, dynamicOverhead, BigNumber.from(l1GasPrice.value)); +} + +function extractMulticallResults( + type: L1FeeCalculationType, + contractCallResults: ContractCallResults +) { + const { results } = contractCallResults; + + if (type === "optimism") { + const returnContexts = results["gasOracle"].callsReturnContext; + const gasUsed = BigNumber.from(returnContexts[0].returnValues[0].hex); + const dynamicOverhead = BigNumber.from(returnContexts[1].returnValues[0] || 684000); + const fixedOverhead = BigNumber.from(returnContexts[2].returnValues[0] || 2100); + + return { gasUsed, fixedOverhead, dynamicOverhead }; + } else if (type === "mantle") { + const [fixedOverhead, dynamicOverhead] = results.gasOracle.callsReturnContext.map( + (call) => call.returnValues + ); + + return { + gasUsed: BigNumber.from(0), + fixedOverhead: BigNumber.from(fixedOverhead), + dynamicOverhead: BigNumber.from(dynamicOverhead), + }; + } + + throw new Error("Invalid type"); +} + +type L1FeeCalculationType = "optimism" | "mantle"; + +function calculateL1Fee( + gasUsed: BigNumber, + fixedOverhead: BigNumber, + dynamicOverhead: BigNumber, + gasPrice: BigNumber +) { + const totalGas = gasUsed.add(fixedOverhead).mul(dynamicOverhead).div(1_000_000); + return totalGas.mul(gasPrice); +} + +function buildContractCallContext( + type: L1FeeCalculationType, + executeData: string +): ContractCallContext[] { + const contractAddress = "0x420000000000000000000000000000000000000F"; + if (type === "optimism") { + return [ + { + reference: "gasOracle", + contractAddress, + abi: ABI.Optimism, + calls: [ + { + reference: "l1GasUsed", + methodName: "getL1GasUsed(bytes)", + methodParameters: [executeData], + }, + { reference: "scalar", methodName: "scalar()", methodParameters: [] }, + { reference: "overhead", methodName: "overhead()", methodParameters: [] }, + ], + }, + ]; + } else if (type === "mantle") { + return [ + { + reference: "gasOracle", + contractAddress, + abi: ABI.Mantle, + calls: [ + { reference: "overhead", methodName: "overhead", methodParameters: [] }, + { reference: "scalar", methodName: "scalar", methodParameters: [] }, + ], + }, + ]; + } + + throw new Error("Invalid contract call type"); +} diff --git a/src/libs/fee/types.ts b/src/libs/fee/types.ts new file mode 100644 index 00000000..cd698df4 --- /dev/null +++ b/src/libs/fee/types.ts @@ -0,0 +1 @@ +export type L2Chain = "optimism" | "arbitrum" | "mantle" | "base" | "scroll"; diff --git a/src/libs/test/AxelarQueryAPI.spec.ts b/src/libs/test/AxelarQueryAPI.spec.ts index 333af864..21acb3c0 100644 --- a/src/libs/test/AxelarQueryAPI.spec.ts +++ b/src/libs/test/AxelarQueryAPI.spec.ts @@ -3,7 +3,7 @@ import { TransferFeeResponse, } from "@axelar-network/axelarjs-types/axelar/nexus/v1beta1/query"; import { BigNumber, BigNumberish, ethers } from "ethers"; -import { parseUnits } from "ethers/lib/utils"; +import { parseEther, parseUnits } from "ethers/lib/utils"; import { CHAINS } from "../../chains"; import { AxelarQueryAPI } from "../AxelarQueryAPI"; import { Environment } from "../types"; @@ -11,6 +11,9 @@ import { EvmChain } from "../../constants/EvmChain"; import { GasToken } from "../../constants/GasToken"; import { activeChainsStub, getFeeStub } from "./stubs"; +const MOCK_EXECUTE_DATA = + "0x1a98b2e0e68ba0eb84262d4bcf91955ec2680b37bcedd59a1f48e18d183dac9961bf9d1400000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000d40000000000000000000000000000000000000000000000000000000000deac2c6000000000000000000000000000000000000000000000000000000000000000762696e616e636500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a307863653136463639333735353230616230313337376365374238386635424138433438463844363636000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bc000000000000000000000000000000000000000000000000000000000000000400000000000000000000000004607cad6135d7a119185ebe062d3b369b1b536ef000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000580000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000009200000000000000000000000000000000000000000000000000000000000000a8000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc450000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc800000000000000000000000000000000000000000000000000000000000000640000000000000000000000004fd39c9e151e50580779bd04b1f7ecc310079fd3000000000000000000000000000000000000000000000000000000000deac2c6000000000000000000000000000000000000000000000000000000000dc647500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f40521500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc450000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc80000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc800000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000640000000000000000000000004fd39c9e151e50580779bd04b1f7ecc310079fd3000000000000000000000000000000000000000000000000000000000de83dbf000000000000000000000000000000000000000000000000015d8c7908dbe7130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc80000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004607cad6135d7a119185ebe062d3b369b1b536ef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000761786c5553444300000000000000000000000000000000000000000000000000"; + describe("AxelarQueryAPI", () => { const api = new AxelarQueryAPI({ environment: Environment.TESTNET }); @@ -83,28 +86,85 @@ describe("AxelarQueryAPI", () => { }); }); + describe("estimateL1GasFee", () => { + const mainnetApi = new AxelarQueryAPI({ environment: Environment.MAINNET }); + + test("it should return 0 if the destination chain is not a L2 chain", async () => { + const gasAmount = await mainnetApi.estimateL1GasFee(EvmChain.AVALANCHE, { + executeData: "0x", + l1GasPrice: { + decimals: 18, + value: "32534506865", + }, + }); + expect(gasAmount.toString()).toEqual("0"); + }); + + test("it should return the logical L1 gas fee for a destination L2 chain", async () => { + const gasAmount = await mainnetApi.estimateL1GasFee(EvmChain.OPTIMISM, { + executeData: MOCK_EXECUTE_DATA, + l1GasPrice: { + decimals: 18, + value: "32534506865", + }, + l2Type: "op", + }); + expect(gasAmount.gt(parseEther("0.00001"))).toBeTruthy(); + }); + }); + describe("estimateGasFee", () => { - test.skip("It should return estimated gas amount that makes sense for USDC", async () => { + test("It should return estimated gas amount that makes sense for USDC", async () => { const gasAmount = await api.estimateGasFee( EvmChain.AVALANCHE, - EvmChain.ETHEREUM, - GasToken.USDC, + "ethereum-2", 700000, 1.1, - "500000" + GasToken.USDC, + "500000", + undefined ); // gasAmount should be less than 10k usd, otherwise we handle decimal conversion incorrectly. - expect(ethers.utils.parseUnits("10000", 6).gt(gasAmount as BigNumberish)).toBeTruthy(); + expect(ethers.utils.parseEther("10000").gt(gasAmount as BigNumberish)).toBeTruthy(); + }); + + test("It should include L1 fee for L2 destination chain", async () => { + const mainnetApi = new AxelarQueryAPI({ environment: Environment.MAINNET }); + const gasAmount = await mainnetApi.estimateGasFee( + EvmChain.ETHEREUM, + EvmChain.OPTIMISM, + 529994, + 1, + undefined, + undefined, + MOCK_EXECUTE_DATA + ); + + expect(gasAmount).toBeDefined(); + }); + + it("should be able to return the gas fee when the source chain is L2, but the executeData is undefined ", async () => { + const response = await api.estimateGasFee( + "ethereum-2", + EvmChain.OPTIMISM, + 700000, + 1.1, + GasToken.USDC, + "500000", + undefined + ); + + expect(response).toBeDefined(); }); test("It should return estimated gas amount that makes sense for native token", async () => { const gasAmount = await api.estimateGasFee( CHAINS.TESTNET.AVALANCHE as EvmChain, CHAINS.TESTNET.ETHEREUM as EvmChain, - GasToken.AVAX, 700000, 1.1, + undefined, "5000000000" ); @@ -122,9 +182,9 @@ describe("AxelarQueryAPI", () => { const gasAmount = await api.estimateGasFee( CHAINS.TESTNET.AVALANCHE as EvmChain, CHAINS.TESTNET.ETHEREUM as EvmChain, - GasToken.AVAX, gasLimit.toNumber(), 1.1, + undefined, minGasPrice.toString() ); const destGasFeeWei = parseUnits( @@ -161,9 +221,9 @@ describe("AxelarQueryAPI", () => { const gasAmount = await api.estimateGasFee( CHAINS.TESTNET.AVALANCHE as EvmChain, CHAINS.TESTNET.ETHEREUM as EvmChain, - GasToken.AVAX, gasLimit.toNumber(), 1.1, + undefined, minGasPrice.toString() ); diff --git a/src/libs/test/TransactionRecoveryAPI/AxelarGMPRecoveryAPI.spec.ts b/src/libs/test/TransactionRecoveryAPI/AxelarGMPRecoveryAPI.spec.ts index 52ee5c04..881f71e1 100644 --- a/src/libs/test/TransactionRecoveryAPI/AxelarGMPRecoveryAPI.spec.ts +++ b/src/libs/test/TransactionRecoveryAPI/AxelarGMPRecoveryAPI.spec.ts @@ -7,7 +7,6 @@ import { EvmWalletDetails, } from "../../types"; import { EvmChain } from "../../../constants/EvmChain"; -import { GasToken } from "../../../constants/GasToken"; import { createNetwork, utils } from "@axelar-network/axelar-local-dev"; import { Contract, ContractReceipt, ContractTransaction, ethers, Wallet } from "ethers"; import DistributionExecutable from "../abi/DistributionExecutable.json"; @@ -749,7 +748,6 @@ describe("AxelarGMPRecoveryAPI", () => { tx.transactionHash, EvmChain.AVALANCHE, EvmChain.MOONBEAM, - GasToken.AVAX, 700000, { provider } ); @@ -785,7 +783,6 @@ describe("AxelarGMPRecoveryAPI", () => { tx.transactionHash, EvmChain.AVALANCHE, EvmChain.MOONBEAM, - GasToken.AVAX, 700000, { provider } ); diff --git a/src/libs/test/stubs/index.ts b/src/libs/test/stubs/index.ts index aaa1ba77..b45897ed 100644 --- a/src/libs/test/stubs/index.ts +++ b/src/libs/test/stubs/index.ts @@ -328,7 +328,6 @@ export const getFeeStub = () => ({ usd: 1674.72, }, gas_price: "0.000000023244098897", - gas_price_gwei: "23.244098897", }, destination_base_fee: 0.00514232878886753, }, diff --git a/src/libs/types/index.ts b/src/libs/types/index.ts index 9941d4b8..c0962cae 100644 --- a/src/libs/types/index.ts +++ b/src/libs/types/index.ts @@ -65,6 +65,17 @@ export interface AxelarQueryAPIConfig { debug?: boolean; } +export type FeeToken = { + gas_price: string; + decimals: number; + name: string; + l1_gas_price_in_units?: TokenUnit; + symbol: string; + token_price: { + usd: number; + }; +}; + export interface BaseFeeResponse { success: boolean; apiResponse?: any; @@ -72,16 +83,16 @@ export interface BaseFeeResponse { baseFee: string; expressFee: string; executeGasMultiplier: number; - sourceToken: { - gas_price: string; - decimals: number; + sourceToken: FeeToken; + destToken: FeeToken; + l2_type: "op" | "arb" | "mantle" | undefined; + ethereumToken: { name: string; symbol: string; - }; - destToken: { - gas_price: string; - gas_price_gwei: string; decimals: number; + token_price: { + usd: number; + }; }; expressSupported: boolean; } @@ -155,6 +166,7 @@ export interface TxResult { export interface QueryGasFeeOptions { provider?: ethers.providers.JsonRpcProvider; + gasTokenSymbol?: GasToken | string; gasMultipler?: number; shouldSubtractBaseFee?: boolean; } @@ -225,3 +237,14 @@ export const isNativeToken = ( ): boolean => { return nativeGasTokenSymbol[environment][chain]?.toLowerCase() === selectedToken?.toLowerCase(); }; + +export type TokenUnit = { + value: string; + decimals: number; +}; + +export type EstimateL1FeeParams = { + executeData: `0x${string}`; + l1GasPrice: TokenUnit; + l2Type?: "op" | "arb" | "mantle" | undefined; +};