Skip to content

Commit

Permalink
feat: implement api and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
npty committed Nov 8, 2024
1 parent 417b383 commit ed4b22c
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 5 deletions.
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
40 changes: 36 additions & 4 deletions src/libs/AxelarQueryAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ interface HopParams {
* The multiplier of gas to be used on execution
* @default 'auto' (multiplier used by relayer)
*/
gasMultiplier?: number;
gasMultiplier?: number | "auto";

/**
* Minimum destination gas price
Expand All @@ -88,7 +88,7 @@ interface HopParams {

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

Expand Down Expand Up @@ -499,8 +499,40 @@ export class AxelarQueryAPI {
: l1ExecutionFeeWithMultiplier.add(executionFeeWithMultiplier).add(baseFee).toString();
}

public async estimateMultihopFee(params: HopParams, options: EstimateMultihopFeeOptions) {
const response = this.axelarscanApi.post("estimateGasFeeForNHops", {});
/**
* 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 | AxelarQueryAPIFeeResponse> {
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,
});

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

/**
Expand Down
187 changes: 187 additions & 0 deletions src/libs/test/AxelarQueryAPI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Environment } from "../types";
import { EvmChain } from "../../constants/EvmChain";
import { GasToken } from "../../constants/GasToken";
import { activeChainsStub, getFeeStub } from "./stubs";
import { SpyInstance } from "vitest";

const MOCK_EXECUTE_DATA =
"0x1a98b2e0e68ba0eb84262d4bcf91955ec2680b37bcedd59a1f48e18d183dac9961bf9d1400000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000d40000000000000000000000000000000000000000000000000000000000deac2c6000000000000000000000000000000000000000000000000000000000000000762696e616e636500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a307863653136463639333735353230616230313337376365374238386635424138433438463844363636000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bc000000000000000000000000000000000000000000000000000000000000000400000000000000000000000004607cad6135d7a119185ebe062d3b369b1b536ef000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000580000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000009200000000000000000000000000000000000000000000000000000000000000a8000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc450000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc800000000000000000000000000000000000000000000000000000000000000640000000000000000000000004fd39c9e151e50580779bd04b1f7ecc310079fd3000000000000000000000000000000000000000000000000000000000deac2c6000000000000000000000000000000000000000000000000000000000dc647500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f40521500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc450000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc80000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc800000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000640000000000000000000000004fd39c9e151e50580779bd04b1f7ecc310079fd3000000000000000000000000000000000000000000000000000000000de83dbf000000000000000000000000000000000000000000000000015d8c7908dbe7130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc80000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004607cad6135d7a119185ebe062d3b369b1b536ef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000761786c5553444300000000000000000000000000000000000000000000000000";
Expand Down Expand Up @@ -118,6 +119,192 @@ describe("AxelarQueryAPI", () => {
});
});

describe("estimateMultihopFee", () => {
let amplifierApi: AxelarQueryAPI;
let mockAxelarscanApiPost: SpyInstance;
let mockThrowIfInvalidChainIds: SpyInstance;

beforeEach(async () => {
amplifierApi = new AxelarQueryAPI({
environment: Environment.DEVNET,
});
// Use vi instead of jest
mockAxelarscanApiPost = vi
.spyOn(amplifierApi["axelarscanApi"], "post")
.mockResolvedValue({ test: "response" });
mockThrowIfInvalidChainIds = vi
.spyOn(await import("../../utils/validateChain"), "throwIfInvalidChainIds")
.mockResolvedValue(undefined);
});

afterEach(() => {
vitest.restoreAllMocks();
});

test("should call API with correct path and parameters", async () => {
const hops = [
{
destinationChain: "avalanche-fuji",
sourceChain: "axelarnet",
gasLimit: "700000",
},
];

await amplifierApi.estimateMultihopFee(hops);

expect(mockAxelarscanApiPost).toHaveBeenCalledWith("/gmp/estimateGasFeeForNHops", {
params: hops,
showDetailedFees: false,
});
});

test("should validate chains before making API call", async () => {
const hops = [
{
destinationChain: "avalanche-fuji",
sourceChain: "axelarnet",
gasLimit: "700000",
},
];

await amplifierApi.estimateMultihopFee(hops);

expect(mockThrowIfInvalidChainIds).toHaveBeenCalledWith(
["avalanche-fuji", "axelarnet"],
Environment.DEVNET
);
});

test("should deduplicate chains for validation", async () => {
const hops = [
{
destinationChain: "avalanche-fuji",
sourceChain: "axelarnet",
gasLimit: "700000",
},
{
destinationChain: "avalanche-fuji",
sourceChain: "axelarnet",
gasLimit: "700000",
},
];

await amplifierApi.estimateMultihopFee(hops);

expect(mockThrowIfInvalidChainIds).toHaveBeenCalledWith(
["avalanche-fuji", "axelarnet"],
Environment.DEVNET
);
});

test("should throw error for empty hops array", async () => {
await expect(amplifierApi.estimateMultihopFee([])).rejects.toThrow(
"At least one hop parameter must be provided"
);

expect(mockThrowIfInvalidChainIds).not.toHaveBeenCalled();
expect(mockAxelarscanApiPost).not.toHaveBeenCalled();
});

test("should pass showDetailedFees option to API", async () => {
const hops = [
{
destinationChain: "avalanche-fuji",
sourceChain: "axelarnet",
gasLimit: "700000",
},
];

await amplifierApi.estimateMultihopFee(hops, { showDetailedFees: true });

expect(mockAxelarscanApiPost).toHaveBeenCalledWith("/gmp/estimateGasFeeForNHops", {
params: hops,
showDetailedFees: true,
});
});

test("should retain original input array order", async () => {
const hops = [
{
destinationChain: "avalanche-fuji",
sourceChain: "axelarnet",
gasLimit: "700000",
},
{
destinationChain: "sui-test2",
sourceChain: "polygon-mumbai",
gasLimit: "800000",
},
];

const originalHops = [...hops];

await amplifierApi.estimateMultihopFee(hops);

expect(hops).toEqual(originalHops);
expect(mockAxelarscanApiPost).toHaveBeenCalledWith("/gmp/estimateGasFeeForNHops", {
params: originalHops,
showDetailedFees: false,
});
});

test("should chain validation fail before API call", async () => {
mockThrowIfInvalidChainIds.mockRejectedValueOnce(new Error("Invalid chain"));

const hops = [
{
destinationChain: "invalid-chain",
sourceChain: "axelarnet",
gasLimit: "700000",
},
];

await expect(amplifierApi.estimateMultihopFee(hops)).rejects.toThrow("Invalid chain");

expect(mockAxelarscanApiPost).not.toHaveBeenCalled();
});

test("should return API response directly", async () => {
const mockResponse = { test: "response" };
mockAxelarscanApiPost.mockResolvedValueOnce(mockResponse);

const hops = [
{
destinationChain: "avalanche-fuji",
sourceChain: "axelarnet",
gasLimit: "700000",
},
];

const response = await amplifierApi.estimateMultihopFee(hops);

expect(response).toBe(mockResponse);
});
});

// describe.only("estimateMultihopFee", () => {
// const amplifierApi = new AxelarQueryAPI({
// environment: Environment.DEVNET,
// });
//
// test("It should return estimated gas fee", async () => {
// const fee = await amplifierApi.estimateMultihopFee([
// {
// destinationChain: "avalanche-fuji",
// sourceChain: "axelarnet",
// gasLimit: "700000",
// },
// {
// destinationChain: "sui-test2",
// sourceChain: "axelarnet",
// gasLimit: "700000",
// },
// ]);
//
// expect(fee).toBeDefined();
// });
// });
//
describe("estimateGasFee", () => {
test("It should return estimated gas amount that makes sense for USDC", async () => {
const gasAmount = await api.estimateGasFee(
Expand Down
2 changes: 2 additions & 0 deletions src/utils/validateChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export async function validateChainIdentifier(chainIdentifier: string, environme
};

const chainIdentifiers = Object.keys(s3.chains);
const axelarIdentifier = s3["axelar"].axelarId;
chainIdentifiers.push(axelarIdentifier);

const foundChain = chainIdentifiers.find(
(identifier: string) => identifier === chainIdentifier.toLowerCase()
Expand Down

0 comments on commit ed4b22c

Please sign in to comment.