Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add estimateMultihopFee function #336

Merged
merged 9 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/chains/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function loadChains(config: LoadChainConfig) {

const s3UrlMap: Record<Environment, string> = {
"devnet-amplifier":
"https://axelar-devnet-amplifier.s3.us-east-2.amazonaws.com/configs/devnet-amplifier-config-1.x.json",
"https://raw.githubusercontent.com/axelarnetwork/axelar-contract-deployments/refs/heads/main/axelar-chains-config/info/devnet-amplifier.json",
testnet: "https://axelar-testnet.s3.us-east-2.amazonaws.com/configs/testnet-config-1.x.json",
mainnet: "https://axelar-mainnet.s3.us-east-2.amazonaws.com/configs/mainnet-config-1.x.json",
};
Expand Down
13 changes: 13 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ const devnetConfigs: EnvironmentConfigs = {
axelarscanUrl: "",
wssStatus: "",
};
const devnetAmplifierConfigs: EnvironmentConfigs = {
resourceUrl: "https://nest-server-testnet.axelar.dev",
axelarRpcUrl: "http://devnet-amplifier.axelar.dev:26657",
axelarLcdUrl: "http://devnet-amplifier.axelar.dev:1317",
axelarGMPApiUrl: "https://devnet-amplifier.api.gmp.axelarscan.io",
axelarscanBaseApiUrl: "https://devnet-amplifier.api.axelarscan.io",
depositServiceUrl: "",
recoveryApiUrl: "",
axelarCrosschainApiUrl: "",
axelarscanUrl: "https://devnet-amplifier.axelarscan.io",
wssStatus: "",
};
const testnetConfigs: EnvironmentConfigs = {
resourceUrl: "https://nest-server-testnet.axelar.dev",
axelarRpcUrl: "https://testnet.rpc.axelar.dev/chain/axelar",
Expand Down Expand Up @@ -64,6 +76,7 @@ const mainnetConfigs: EnvironmentConfigs = {

configsMap["local"] = localConfigs;
configsMap["devnet"] = devnetConfigs;
configsMap["devnet-amplifier"] = devnetAmplifierConfigs;
configsMap["testnet"] = testnetConfigs;
configsMap["mainnet"] = mainnetConfigs;

Expand Down
167 changes: 166 additions & 1 deletion src/libs/AxelarQueryAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
TransferFeeResponse,
} from "@axelar-network/axelarjs-types/axelar/nexus/v1beta1/query";
import { throwIfInvalidChainIds } from "../utils";
import { loadChains } from "../chains";
import { importS3Config, loadChains } from "../chains";

Check warning on line 23 in src/libs/AxelarQueryAPI.ts

View workflow job for this annotation

GitHub Actions / ESLint

src/libs/AxelarQueryAPI.ts#L23

'importS3Config' is defined but never used (@typescript-eslint/no-unused-vars)
import s3 from "./TransactionRecoveryApi/constants/s3";
import { BigNumber, BigNumberish, ethers } from "ethers";
import { ChainInfo } from "../chains/types";
Expand Down Expand Up @@ -55,14 +55,104 @@
apiResponse: any;
isExpressSupported: boolean;
}

interface HopParams {
/** The destination chain for the GMP transaction */
destinationChain: string;

/** The source chain for the GMP transaction */
sourceChain: string;

/**
* The gasLimit needed for execution on the destination chain.
* For OP Stack chains (Optimism, Base, Scroll, Fraxtal, Blast, Mantle, etc.),
* only specify the gasLimit for L2 (L2GasLimit).
* The endpoint estimates and bundles the gas needed for L1 (L1GasLimit) automatically.
*/
gasLimit: string;

/**
* The multiplier of gas to be used on execution
* @default 'auto' (multiplier used by relayer)
*/
gasMultiplier?: number | "auto";

/**
* Minimum destination gas price
* @default minimum gas price used by relayer
*/
minGasPrice?: string;

/** The token symbol on the source chain */
sourceTokenSymbol?: string;

/**
* The token address on the source chain
* @default "ZeroAddress" for native token
*/
sourceTokenAddress?: string;

/** Source address for checking if express is supported */
sourceContractAddress?: string;

/** The payload that will be used on destination */
executeData?: string;

/** Destination contract address for checking if express is supported */
destinationContractAddress?: string;

/** Symbol that is used in callContractWithToken for checking if express is supported */
symbol?: string;

/**
* Token amount (in units) that is used in callContractWithToken for checking if express is supported
*/
amountInUnits?: string;
}

/**
* Represents detailed fee information for a single hop
*/
interface HopFeeDetails {
isExpressSupported: boolean;
baseFee: string;
expressFee: string;
executionFee: string;
executionFeeWithMultiplier: string;
totalFee: string;
gasLimit: string;
gasLimitWithL1Fee: string;
gasMultiplier: number;
minGasPrice: string;
}

/**
* Response for fee estimation with detailed breakdown
*/
export interface DetailedFeeResponse {
isExpressSupported: boolean;
baseFee: string;
expressFee: string;
executionFee: string;
executionFeeWithMultiplier: string;
totalFee: string;
details?: HopFeeDetails[];
}

interface EstimateMultihopFeeOptions {
showDetailedFees?: boolean;
}

export class AxelarQueryAPI {
readonly environment: Environment;
readonly lcdApi: RestService;
readonly rpcApi: RestService;
readonly axelarGMPServiceApi: RestService;
readonly axelarscanApi: RestService;
readonly axelarRpcUrl: string;
readonly axelarLcdUrl: string;
readonly axelarGMPServiceUrl: string;
readonly axelarscanBaseApiUrl: string;
private allAssets: AssetConfig[];
private axelarQueryClient: AxelarQueryClientType;
private chainsList: ChainInfo[] = [];
Expand All @@ -74,11 +164,13 @@
this.axelarRpcUrl = axelarRpcUrl || links.axelarRpcUrl;
this.axelarLcdUrl = axelarLcdUrl || links.axelarLcdUrl;
this.axelarGMPServiceUrl = links.axelarGMPApiUrl;
this.axelarscanBaseApiUrl = links.axelarscanBaseApiUrl;
this.environment = environment;

this.lcdApi = new RestService(this.axelarLcdUrl);
this.rpcApi = new RestService(this.axelarRpcUrl);
this.axelarGMPServiceApi = new RestService(this.axelarGMPServiceUrl);
this.axelarscanApi = new RestService(this.axelarscanBaseApiUrl);

this._initializeAssets();
}
Expand Down Expand Up @@ -436,6 +528,46 @@
: l1ExecutionFeeWithMultiplier.add(executionFeeWithMultiplier).add(baseFee).toString();
}

/**
* Estimates the total gas fee for a multi-hop GMP transfer via Axelar
* @param hops Array of hop parameters defining each step of the transfer path
* @param options Optional parameters for fee estimation
* @throws {Error} If no hops are provided or chain validation fails
* @returns Promise containing the estimated fees if the showDetailedFees option is not provided, or an object containing the detailed fees if showDetailedFees is true
*/
public async estimateMultihopFee(
hops: HopParams[],
options?: EstimateMultihopFeeOptions
): Promise<string | DetailedFeeResponse> {
if (hops.length === 0) {
throw new Error("At least one hop parameter must be provided");
}

const chainsToValidate = Array.from(
new Set([...hops.map((hop) => hop.destinationChain), ...hops.map((hop) => hop.sourceChain)])
);

await throwIfInvalidChainIds(chainsToValidate, this.environment);

try {
const response = await this.axelarscanApi.post("/gmp/estimateGasFeeForNHops", {
params: hops,
showDetailedFees: options?.showDetailedFees ?? false,
});

if (options?.showDetailedFees) {
return this.mapToDetailedFeeResponse(response);
}

return response;
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to estimate multi-hop gas fee: ${error.message}`);
}
throw error;
}
}

/**
* Get the denom for an asset given its symbol on a chain
* @param symbol
Expand Down Expand Up @@ -666,4 +798,37 @@
if (!assetConfig) throw `Asset ${denom} not found`;
return assetConfig.wrapped_erc20 ? assetConfig.wrapped_erc20 : denom;
}

/**
* Maps raw API response to simplified hop fee details
*/
private mapToHopFeeDetails(rawHopDetails: any): HopFeeDetails {
return {
isExpressSupported: rawHopDetails.isExpressSupported,
baseFee: rawHopDetails.baseFee,
expressFee: rawHopDetails.expressFee,
executionFee: rawHopDetails.executionFee,
executionFeeWithMultiplier: rawHopDetails.executionFeeWithMultiplier,
totalFee: rawHopDetails.totalFee,
gasLimit: rawHopDetails.gasLimit,
gasLimitWithL1Fee: rawHopDetails.gasLimitWithL1Fee,
gasMultiplier: rawHopDetails.gasMultiplier,
minGasPrice: rawHopDetails.minGasPrice,
};
}

/**
* Maps raw API response to simplified detailed fee response
*/
private mapToDetailedFeeResponse(rawResponse: any): DetailedFeeResponse {
return {
isExpressSupported: rawResponse.isExpressSupported,
baseFee: rawResponse.baseFee,
expressFee: rawResponse.expressFee,
executionFee: rawResponse.executionFee,
executionFeeWithMultiplier: rawResponse.executionFeeWithMultiplier,
totalFee: rawResponse.totalFee,
details: rawResponse.details?.map(this.mapToHopFeeDetails),
};
}
}
Loading
Loading