| 
 | 1 | +import {  | 
 | 2 | +  getAxelarApiUrl,  | 
 | 3 | +  getAxelarChain,  | 
 | 4 | +  parseGMPStatus,  | 
 | 5 | +  parseGMPError,  | 
 | 6 | +  getAxelarGasFee,  | 
 | 7 | +  getAxelarTransactionStatus,  | 
 | 8 | +  GMPStatus,  | 
 | 9 | +} from "../src/axelar.js";  | 
 | 10 | + | 
 | 11 | +describe("Axelar Utility Functions", () => {  | 
 | 12 | +  describe("getAxelarApiUrl", () => {  | 
 | 13 | +    it("should return mainnet API URL for Mainnet network", () => {  | 
 | 14 | +      const url = getAxelarApiUrl("Mainnet");  | 
 | 15 | +      expect(url).toBe("https://api.axelarscan.io");  | 
 | 16 | +    });  | 
 | 17 | + | 
 | 18 | +    it("should return testnet API URL for Testnet network", () => {  | 
 | 19 | +      const url = getAxelarApiUrl("Testnet");  | 
 | 20 | +      expect(url).toBe("https://testnet.api.axelarscan.io");  | 
 | 21 | +    });  | 
 | 22 | +  });  | 
 | 23 | + | 
 | 24 | +  describe("getAxelarChain", () => {  | 
 | 25 | +    it("should return axelar chain name for Ethereum", () => {  | 
 | 26 | +      const chain = getAxelarChain("Ethereum");  | 
 | 27 | +      expect(chain).toBe("ethereum");  | 
 | 28 | +    });  | 
 | 29 | + | 
 | 30 | +    it("should return axelar chain name for Monad", () => {  | 
 | 31 | +      const chain = getAxelarChain("Monad");  | 
 | 32 | +      expect(chain).toBe("monad");  | 
 | 33 | +    });  | 
 | 34 | + | 
 | 35 | +    it("should return axelar chain name for Sepolia", () => {  | 
 | 36 | +      const chain = getAxelarChain("Sepolia");  | 
 | 37 | +      expect(chain).toBe("ethereum-sepolia");  | 
 | 38 | +    });  | 
 | 39 | + | 
 | 40 | +    it("should throw error for unsupported chain", () => {  | 
 | 41 | +      expect(() => getAxelarChain("Solana" as any)).toThrow(  | 
 | 42 | +        "Unsupported axelar chain: Solana"  | 
 | 43 | +      );  | 
 | 44 | +    });  | 
 | 45 | +  });  | 
 | 46 | + | 
 | 47 | +  describe("parseGMPStatus", () => {  | 
 | 48 | +    it("should parse error status", () => {  | 
 | 49 | +      const response = { status: "error", error: { message: "some error" } };  | 
 | 50 | +      const status = parseGMPStatus(response);  | 
 | 51 | +      expect(status).toBe(GMPStatus.DEST_EXECUTE_ERROR);  | 
 | 52 | +    });  | 
 | 53 | + | 
 | 54 | +    it("should parse executed status", () => {  | 
 | 55 | +      const response = { status: "executed" };  | 
 | 56 | +      const status = parseGMPStatus(response);  | 
 | 57 | +      expect(status).toBe(GMPStatus.DEST_EXECUTED);  | 
 | 58 | +    });  | 
 | 59 | + | 
 | 60 | +    it("should parse called status", () => {  | 
 | 61 | +      const response = { status: "called" };  | 
 | 62 | +      const status = parseGMPStatus(response);  | 
 | 63 | +      expect(status).toBe(GMPStatus.SRC_GATEWAY_CALLED);  | 
 | 64 | +    });  | 
 | 65 | + | 
 | 66 | +    it("should parse executing status", () => {  | 
 | 67 | +      const response = { status: "executing" };  | 
 | 68 | +      const status = parseGMPStatus(response);  | 
 | 69 | +      expect(status).toBe(GMPStatus.DEST_EXECUTING);  | 
 | 70 | +    });  | 
 | 71 | +  });  | 
 | 72 | + | 
 | 73 | +  describe("parseGMPError", () => {  | 
 | 74 | +    it("should parse error from response", () => {  | 
 | 75 | +      const response = {  | 
 | 76 | +        error: {  | 
 | 77 | +          error: { message: "execution failed" },  | 
 | 78 | +          sourceTransactionHash: "0xabc123",  | 
 | 79 | +          chain: "ethereum",  | 
 | 80 | +        },  | 
 | 81 | +      };  | 
 | 82 | +      const error = parseGMPError(response);  | 
 | 83 | +      expect(error).toEqual({  | 
 | 84 | +        message: "execution failed",  | 
 | 85 | +        txHash: "0xabc123",  | 
 | 86 | +        chain: "ethereum",  | 
 | 87 | +      });  | 
 | 88 | +    });  | 
 | 89 | + | 
 | 90 | +    it("should parse insufficient fee error", () => {  | 
 | 91 | +      const response = {  | 
 | 92 | +        is_insufficient_fee: true,  | 
 | 93 | +        call: {  | 
 | 94 | +          transaction: { hash: "0xdef456" },  | 
 | 95 | +          chain: "monad",  | 
 | 96 | +        },  | 
 | 97 | +      };  | 
 | 98 | +      const error = parseGMPError(response);  | 
 | 99 | +      expect(error).toEqual({  | 
 | 100 | +        message: "Insufficient gas",  | 
 | 101 | +        txHash: "0xdef456",  | 
 | 102 | +        chain: "monad",  | 
 | 103 | +      });  | 
 | 104 | +    });  | 
 | 105 | + | 
 | 106 | +    it("should return undefined when no error", () => {  | 
 | 107 | +      const response = { status: "executed" };  | 
 | 108 | +      const error = parseGMPError(response);  | 
 | 109 | +      expect(error).toBeUndefined();  | 
 | 110 | +    });  | 
 | 111 | +  });  | 
 | 112 | + | 
 | 113 | +  describe("getAxelarGasFee", () => {  | 
 | 114 | +    const originalFetch = global.fetch;  | 
 | 115 | +    let mockFetch: jest.Mock;  | 
 | 116 | + | 
 | 117 | +    beforeEach(() => {  | 
 | 118 | +      mockFetch = jest.fn() as jest.Mock;  | 
 | 119 | +      global.fetch = mockFetch as any;  | 
 | 120 | +    });  | 
 | 121 | + | 
 | 122 | +    afterEach(() => {  | 
 | 123 | +      global.fetch = originalFetch;  | 
 | 124 | +    });  | 
 | 125 | + | 
 | 126 | +    it("should fetch and return gas fee", async () => {  | 
 | 127 | +      const mockResponse = {  | 
 | 128 | +        ok: true,  | 
 | 129 | +        json: async () => "1000000000000000",  | 
 | 130 | +      };  | 
 | 131 | +      mockFetch.mockResolvedValue(mockResponse);  | 
 | 132 | + | 
 | 133 | +      const fee = await getAxelarGasFee("Testnet", "Sepolia", "Monad", 500000n);  | 
 | 134 | +      expect(fee).toBe(1000000000000000n);  | 
 | 135 | +      expect(global.fetch).toHaveBeenCalledWith(  | 
 | 136 | +        "https://testnet.api.axelarscan.io/gmp/estimateGasFee",  | 
 | 137 | +        expect.objectContaining({  | 
 | 138 | +          method: "POST",  | 
 | 139 | +          headers: { "Content-Type": "application/json" },  | 
 | 140 | +          body: JSON.stringify({  | 
 | 141 | +            sourceChain: "ethereum-sepolia",  | 
 | 142 | +            destinationChain: "monad",  | 
 | 143 | +            gasMultiplier: "auto",  | 
 | 144 | +            gasLimit: "500000",  | 
 | 145 | +          }),  | 
 | 146 | +        })  | 
 | 147 | +      );  | 
 | 148 | +    });  | 
 | 149 | + | 
 | 150 | +    it("should return minimum fee of 1 when API returns 0", async () => {  | 
 | 151 | +      const mockResponse = {  | 
 | 152 | +        ok: true,  | 
 | 153 | +        json: async () => "0",  | 
 | 154 | +      };  | 
 | 155 | +      mockFetch.mockResolvedValue(mockResponse);  | 
 | 156 | + | 
 | 157 | +      const fee = await getAxelarGasFee("Testnet", "Sepolia", "Monad", 500000n);  | 
 | 158 | +      expect(fee).toBe(1n);  | 
 | 159 | +    });  | 
 | 160 | + | 
 | 161 | +    it("should throw error when API call fails", async () => {  | 
 | 162 | +      const mockResponse = {  | 
 | 163 | +        ok: false,  | 
 | 164 | +        status: 500,  | 
 | 165 | +        text: async () => "Internal server error",  | 
 | 166 | +      };  | 
 | 167 | +      mockFetch.mockResolvedValue(mockResponse);  | 
 | 168 | + | 
 | 169 | +      await expect(  | 
 | 170 | +        getAxelarGasFee("Testnet", "Sepolia", "Monad", 500000n)  | 
 | 171 | +      ).rejects.toThrow("Failed to estimate gas fee: 500");  | 
 | 172 | +    });  | 
 | 173 | + | 
 | 174 | +    it("should throw error when request times out", async () => {  | 
 | 175 | +      mockFetch.mockImplementation(() => {  | 
 | 176 | +        return new Promise((_, reject) => {  | 
 | 177 | +          setTimeout(() => reject(new Error("Timeout")), 100);  | 
 | 178 | +        });  | 
 | 179 | +      });  | 
 | 180 | + | 
 | 181 | +      await expect(  | 
 | 182 | +        getAxelarGasFee("Testnet", "Sepolia", "Monad", 500000n, 50)  | 
 | 183 | +      ).rejects.toThrow("Timeout");  | 
 | 184 | +    });  | 
 | 185 | + | 
 | 186 | +    it("should use mainnet API for Mainnet network", async () => {  | 
 | 187 | +      const mockResponse = {  | 
 | 188 | +        ok: true,  | 
 | 189 | +        json: async () => "2000000000000000",  | 
 | 190 | +      };  | 
 | 191 | +      mockFetch.mockResolvedValue(mockResponse);  | 
 | 192 | + | 
 | 193 | +      await getAxelarGasFee("Mainnet", "Ethereum", "Monad", 500000n);  | 
 | 194 | +      expect(global.fetch).toHaveBeenCalledWith(  | 
 | 195 | +        "https://api.axelarscan.io/gmp/estimateGasFee",  | 
 | 196 | +        expect.any(Object)  | 
 | 197 | +      );  | 
 | 198 | +    });  | 
 | 199 | +  });  | 
 | 200 | + | 
 | 201 | +  describe("getAxelarTransactionStatus", () => {  | 
 | 202 | +    const originalFetch = global.fetch;  | 
 | 203 | +    let mockFetch: jest.Mock;  | 
 | 204 | + | 
 | 205 | +    beforeEach(() => {  | 
 | 206 | +      mockFetch = jest.fn() as jest.Mock;  | 
 | 207 | +      global.fetch = mockFetch as any;  | 
 | 208 | +    });  | 
 | 209 | + | 
 | 210 | +    afterEach(() => {  | 
 | 211 | +      global.fetch = originalFetch;  | 
 | 212 | +    });  | 
 | 213 | + | 
 | 214 | +    it("should fetch and return transaction status", async () => {  | 
 | 215 | +      const mockResponse = {  | 
 | 216 | +        ok: true,  | 
 | 217 | +        json: async () => ({  | 
 | 218 | +          data: [  | 
 | 219 | +            {  | 
 | 220 | +              status: "executed",  | 
 | 221 | +            },  | 
 | 222 | +          ],  | 
 | 223 | +        }),  | 
 | 224 | +      };  | 
 | 225 | +      mockFetch.mockResolvedValue(mockResponse);  | 
 | 226 | + | 
 | 227 | +      const result = await getAxelarTransactionStatus(  | 
 | 228 | +        "Testnet",  | 
 | 229 | +        "Sepolia",  | 
 | 230 | +        "0xabc123"  | 
 | 231 | +      );  | 
 | 232 | +      expect(result.status).toBe(GMPStatus.DEST_EXECUTED);  | 
 | 233 | +      expect(result.error).toBeUndefined();  | 
 | 234 | +      expect(global.fetch).toHaveBeenCalledWith(  | 
 | 235 | +        "https://testnet.api.axelarscan.io/gmp/searchGMP",  | 
 | 236 | +        expect.objectContaining({  | 
 | 237 | +          method: "POST",  | 
 | 238 | +          headers: { "Content-Type": "application/json" },  | 
 | 239 | +          body: JSON.stringify({  | 
 | 240 | +            sourceChain: "ethereum-sepolia",  | 
 | 241 | +            txHash: "0xabc123",  | 
 | 242 | +          }),  | 
 | 243 | +        })  | 
 | 244 | +      );  | 
 | 245 | +    });  | 
 | 246 | + | 
 | 247 | +    it("should return status with error when present", async () => {  | 
 | 248 | +      const mockResponse = {  | 
 | 249 | +        ok: true,  | 
 | 250 | +        json: async () => ({  | 
 | 251 | +          data: [  | 
 | 252 | +            {  | 
 | 253 | +              status: "error",  | 
 | 254 | +              error: {  | 
 | 255 | +                error: { message: "execution reverted" },  | 
 | 256 | +                sourceTransactionHash: "0xabc123",  | 
 | 257 | +                chain: "ethereum",  | 
 | 258 | +              },  | 
 | 259 | +            },  | 
 | 260 | +          ],  | 
 | 261 | +        }),  | 
 | 262 | +      };  | 
 | 263 | +      mockFetch.mockResolvedValue(mockResponse);  | 
 | 264 | + | 
 | 265 | +      const result = await getAxelarTransactionStatus(  | 
 | 266 | +        "Testnet",  | 
 | 267 | +        "Sepolia",  | 
 | 268 | +        "0xabc123"  | 
 | 269 | +      );  | 
 | 270 | +      expect(result.status).toBe(GMPStatus.DEST_EXECUTE_ERROR);  | 
 | 271 | +      expect(result.error).toEqual({  | 
 | 272 | +        message: "execution reverted",  | 
 | 273 | +        txHash: "0xabc123",  | 
 | 274 | +        chain: "ethereum",  | 
 | 275 | +      });  | 
 | 276 | +    });  | 
 | 277 | + | 
 | 278 | +    it("should throw error when no transaction details found", async () => {  | 
 | 279 | +      const mockResponse = {  | 
 | 280 | +        ok: true,  | 
 | 281 | +        json: async () => ({  | 
 | 282 | +          data: [],  | 
 | 283 | +        }),  | 
 | 284 | +      };  | 
 | 285 | +      mockFetch.mockResolvedValue(mockResponse);  | 
 | 286 | + | 
 | 287 | +      await expect(  | 
 | 288 | +        getAxelarTransactionStatus("Testnet", "Sepolia", "0xabc123")  | 
 | 289 | +      ).rejects.toThrow("No transaction details found");  | 
 | 290 | +    });  | 
 | 291 | + | 
 | 292 | +    it("should throw error when API call fails", async () => {  | 
 | 293 | +      const mockResponse = {  | 
 | 294 | +        ok: false,  | 
 | 295 | +        status: 500,  | 
 | 296 | +        text: async () => "Internal server error",  | 
 | 297 | +      };  | 
 | 298 | +      mockFetch.mockResolvedValue(mockResponse);  | 
 | 299 | + | 
 | 300 | +      await expect(  | 
 | 301 | +        getAxelarTransactionStatus("Testnet", "Sepolia", "0xabc123")  | 
 | 302 | +      ).rejects.toThrow("Failed to get transaction status: 500");  | 
 | 303 | +    });  | 
 | 304 | + | 
 | 305 | +    it("should throw error when request times out", async () => {  | 
 | 306 | +      mockFetch.mockImplementation(  | 
 | 307 | +        () =>  | 
 | 308 | +          new Promise((_, reject) => {  | 
 | 309 | +            setTimeout(() => reject(new Error("Timeout")), 100);  | 
 | 310 | +          })  | 
 | 311 | +      );  | 
 | 312 | + | 
 | 313 | +      await expect(  | 
 | 314 | +        getAxelarTransactionStatus("Testnet", "Sepolia", "0xabc123", 50)  | 
 | 315 | +      ).rejects.toThrow();  | 
 | 316 | +    });  | 
 | 317 | +  });  | 
 | 318 | +});  | 
0 commit comments