/
ikratkiy
/
Hyperliquid
Обзор
Документация
Войти
/
ikratkiy
/
Hyperliquid
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/shared/hyperliquid/api.test.ts
204 строки
8 KB
ikratkiy
feat(spot): add fail-closed reconciliation contracts
15 июл 2026, 02:22
15 июл 2026, 02:22
eb972fb
Код
Авторство
О чём код?
import { afterEach, describe, expect, test, vi } from "vitest"; import { getOrderStatus, getUserAbstraction, getUserFees, getUserFillsByTime, postBtcToUsdcExchange, postExchange, postJsonWithFallback, postJsonWithoutFallback, } from "./api"; describe("Hyperliquid API fallback", () => { afterEach(() => { vi.restoreAllMocks(); }); test("tries the next endpoint after a transport failure", async () => { const fetchMock = vi .spyOn(globalThis, "fetch") .mockRejectedValueOnce(new Error("network down")) .mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 })); const result = await postJsonWithFallback<{ ok: boolean }>(["https://bad.example", "https://good.example"], "/info", { type: "allMids", }); expect(result.data).toEqual({ ok: true }); expect(result.endpoint).toBe("https://good.example"); expect(result.attempts).toHaveLength(2); expect(fetchMock).toHaveBeenNthCalledWith(2, "https://good.example/info", expect.objectContaining({ method: "POST" })); }); test("does not fallback on valid Hyperliquid business error response", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( new Response( JSON.stringify({ status: "ok", response: { type: "order", data: { statuses: [{ error: "Order must have minimum value of $10." }] } }, }), { status: 200 }, ), ); const result = await postJsonWithFallback<unknown>(["https://first.example", "https://second.example"], "/exchange", { action: {}, nonce: 1, signature: {}, }); expect(result.endpoint).toBe("https://first.example"); expect(result.attempts).toHaveLength(1); }); test("does not retry a write after an ambiguous transport failure", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("connection reset")); await expect( postJsonWithoutFallback("https://first.example", "/exchange", { action: { type: "order" }, nonce: 1, signature: {}, }), ).rejects.toThrow("Write request was not retried because its acceptance status is unknown"); expect(fetchMock).toHaveBeenCalledTimes(1); }); test("blocks the legacy exchange client before a mainnet HTTP request", async () => { const fetchMock = vi.spyOn(globalThis, "fetch"); await expect(postExchange("mainnet", { action: { type: "order" }, nonce: 1, signature: {} })).rejects.toThrow( "disabled for this environment and legacy flow", ); expect(fetchMock).not.toHaveBeenCalled(); }); test("allows the scenario-scoped client only on mainnet", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); const envelope = { action: { type: "order" }, nonce: 1, signature: {} }; await expect(postBtcToUsdcExchange("testnet", envelope)).rejects.toThrow("restricted to the documented Mainnet scenario"); await expect(postBtcToUsdcExchange("mainnet", envelope)).resolves.toEqual({ status: "ok" }); expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith("https://api.hyperliquid.xyz/exchange", expect.objectContaining({ method: "POST" })); }); test("requests userAbstraction for lowercase user address", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(JSON.stringify("unifiedAccount"), { status: 200 })); await expect(getUserAbstraction("testnet", "0x99a5F7202c4983a6f0Ca9d8F0526Fcd9d2be9e1D")).resolves.toBe("unifiedAccount"); const request = JSON.parse(fetchMock.mock.calls[0][1]?.body as string); expect(request).toEqual({ type: "userAbstraction", user: "0x99a5f7202c4983a6f0ca9d8f0526fcd9d2be9e1d", }); }); test("requests orderStatus by cloid with the exact lowercase user body", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(JSON.stringify({ status: "unknownOid" }), { status: 200 })); await expect( getOrderStatus( "mainnet", "0x99a5F7202c4983a6f0Ca9d8F0526Fcd9d2be9e1D", "0x00000000000000000000000000000001", ), ).resolves.toEqual({ status: "unknownOid" }); expect(JSON.parse(fetchMock.mock.calls[0][1]?.body as string)).toEqual({ type: "orderStatus", user: "0x99a5f7202c4983a6f0ca9d8f0526fcd9d2be9e1d", oid: "0x00000000000000000000000000000001", }); }); test("requests unaggregated user fills in an exact inclusive time window", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(JSON.stringify([]), { status: 200 })); await expect( getUserFillsByTime("mainnet", "0x99a5F7202c4983a6f0Ca9d8F0526Fcd9d2be9e1D", 1710000000000, 1710000005000), ).resolves.toEqual([]); expect(JSON.parse(fetchMock.mock.calls[0][1]?.body as string)).toEqual({ type: "userFillsByTime", user: "0x99a5f7202c4983a6f0ca9d8f0526fcd9d2be9e1d", startTime: 1710000000000, endTime: 1710000005000, aggregateByTime: false, }); }); test("omits userFillsByTime endTime when the caller does not provide it", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(JSON.stringify([]), { status: 200 })); await getUserFillsByTime("mainnet", "0x99a5F7202c4983a6f0Ca9d8F0526Fcd9d2be9e1D", 1710000000000); expect(JSON.parse(fetchMock.mock.calls[0][1]?.body as string)).toEqual({ type: "userFillsByTime", user: "0x99a5f7202c4983a6f0ca9d8f0526fcd9d2be9e1d", startTime: 1710000000000, aggregateByTime: false, }); }); test("rejects invalid fill windows before a network request", () => { const fetchMock = vi.spyOn(globalThis, "fetch"); expect(() => getUserFillsByTime("mainnet", "0x99a5f7202c4983a6f0ca9d8f0526fcd9d2be9e1d", -1)).toThrow( "startTime", ); expect(() => getUserFillsByTime("mainnet", "0x99a5f7202c4983a6f0ca9d8f0526fcd9d2be9e1d", 10, 9), ).toThrow("endTime"); expect(fetchMock).not.toHaveBeenCalled(); }); test("rejects oid values that the browser cannot represent safely", () => { const fetchMock = vi.spyOn(globalThis, "fetch"); expect(() => getOrderStatus("mainnet", "0x99a5f7202c4983a6f0ca9d8f0526fcd9d2be9e1d", Number.MAX_SAFE_INTEGER + 1), ).toThrow("safe integer"); expect(() => getOrderStatus("mainnet", "0x99a5f7202c4983a6f0ca9d8f0526fcd9d2be9e1d", "0x1234" as `0x${string}`), ).toThrow("16-byte cloid"); expect(fetchMock).not.toHaveBeenCalled(); }); test("requests userFees for the lowercase user address", async () => { const response = { dailyUserVlm: [], feeSchedule: { cross: "0.00045", add: "0.00015", spotCross: "0.0007", spotAdd: "0.0004", tiers: { vip: [], mm: [] }, referralDiscount: "0.04", stakingDiscountTiers: [], }, userCrossRate: "0.00045", userAddRate: "0.00015", userSpotCrossRate: "0.0007", userSpotAddRate: "0.0004", activeReferralDiscount: "0", trial: null, feeTrialReward: "0", nextTrialAvailableTimestamp: null, stakingLink: null, activeStakingDiscount: null, }; const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(JSON.stringify(response), { status: 200 })); await expect(getUserFees("mainnet", "0x99a5F7202c4983a6f0Ca9d8F0526Fcd9d2be9e1D")).resolves.toEqual(response); expect(JSON.parse(fetchMock.mock.calls[0][1]?.body as string)).toEqual({ type: "userFees", user: "0x99a5f7202c4983a6f0ca9d8f0526fcd9d2be9e1d", }); }); });