426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
369 lines
9.0 KiB
TypeScript
369 lines
9.0 KiB
TypeScript
/**
|
|
* Smoke tests for `handleHyperliquidRoute` and `createHyperliquidInfoClient`:
|
|
* markets/funding/status/positions/orders route payloads, non-GET 501
|
|
* execution-disabled responses, and position/order normalization math
|
|
* (mark price, distance-to-liquidation, effective leverage). Fetch is mocked
|
|
* against fixed responses — no live Hyperliquid API calls.
|
|
*/
|
|
import type http from "node:http";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
createHyperliquidInfoClient,
|
|
type HyperliquidFetch,
|
|
handleHyperliquidRoute,
|
|
} from "../src/routes";
|
|
|
|
function responseRecorder() {
|
|
const res = {
|
|
status: 0,
|
|
headers: {} as Record<string, string>,
|
|
body: "",
|
|
headersSent: false,
|
|
statusCode: 0,
|
|
setHeader(key: string, value: string) {
|
|
this.headers[key.toLowerCase()] = value;
|
|
},
|
|
writeHead(status: number, headers?: Record<string, string>) {
|
|
this.status = status;
|
|
this.statusCode = status;
|
|
this.headers = headers ?? {};
|
|
},
|
|
end(body?: string) {
|
|
this.status = this.statusCode;
|
|
this.body = body ?? "";
|
|
this.headersSent = true;
|
|
},
|
|
};
|
|
return res;
|
|
}
|
|
|
|
function fixedNow() {
|
|
return new Date("2026-05-01T12:00:00.000Z");
|
|
}
|
|
|
|
describe("Hyperliquid route and info client behavior", () => {
|
|
it("fetches markets through the Info API and returns parsed route payloads", async () => {
|
|
const fetchImpl = vi.fn(
|
|
async (_input: string | URL | Request, init?: RequestInit) => {
|
|
expect(init).toMatchObject({
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ type: "meta" }),
|
|
});
|
|
return Response.json({
|
|
universe: [
|
|
{ name: "BTC", szDecimals: 5, maxLeverage: 50 },
|
|
{
|
|
name: "ETH",
|
|
szDecimals: 4,
|
|
onlyIsolated: true,
|
|
isDelisted: false,
|
|
},
|
|
],
|
|
});
|
|
},
|
|
) as HyperliquidFetch;
|
|
const res = responseRecorder();
|
|
|
|
await expect(
|
|
handleHyperliquidRoute(
|
|
{} as http.IncomingMessage,
|
|
res as unknown as http.ServerResponse,
|
|
"/api/hyperliquid/markets",
|
|
"GET",
|
|
{ fetchImpl, now: fixedNow },
|
|
),
|
|
).resolves.toBe(true);
|
|
|
|
expect(fetchImpl).toHaveBeenCalledWith(
|
|
"https://api.hyperliquid.xyz/info",
|
|
expect.any(Object),
|
|
);
|
|
expect(res.status).toBe(200);
|
|
expect(JSON.parse(res.body)).toEqual({
|
|
markets: [
|
|
{
|
|
name: "BTC",
|
|
index: 0,
|
|
szDecimals: 5,
|
|
maxLeverage: 50,
|
|
onlyIsolated: false,
|
|
isDelisted: false,
|
|
},
|
|
{
|
|
name: "ETH",
|
|
index: 1,
|
|
szDecimals: 4,
|
|
maxLeverage: null,
|
|
onlyIsolated: true,
|
|
isDelisted: false,
|
|
},
|
|
],
|
|
source: "hyperliquid-info-meta",
|
|
fetchedAt: "2026-05-01T12:00:00.000Z",
|
|
});
|
|
});
|
|
|
|
it("fetches current funding rates through metaAndAssetCtxs", async () => {
|
|
const fetchImpl = vi.fn(
|
|
async (_input: string | URL | Request, init?: RequestInit) => {
|
|
expect(init).toMatchObject({
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ type: "metaAndAssetCtxs" }),
|
|
});
|
|
return Response.json([
|
|
{
|
|
universe: [
|
|
{ name: "BTC", szDecimals: 5, maxLeverage: 50 },
|
|
{ name: "ETH", szDecimals: 4, maxLeverage: 50 },
|
|
],
|
|
},
|
|
[
|
|
{
|
|
funding: "0.0000125",
|
|
premium: "0.00031774",
|
|
markPx: "14.3161",
|
|
oraclePx: "14.32",
|
|
openInterest: "688.11",
|
|
},
|
|
{
|
|
funding: "-0.000001",
|
|
premium: "-0.00002",
|
|
markPx: "6.0436",
|
|
oraclePx: "6.0457",
|
|
openInterest: "1882.55",
|
|
},
|
|
],
|
|
]);
|
|
},
|
|
) as HyperliquidFetch;
|
|
const res = responseRecorder();
|
|
|
|
await expect(
|
|
handleHyperliquidRoute(
|
|
{} as http.IncomingMessage,
|
|
res as unknown as http.ServerResponse,
|
|
"/api/hyperliquid/funding",
|
|
"GET",
|
|
{ fetchImpl, now: fixedNow },
|
|
),
|
|
).resolves.toBe(true);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(JSON.parse(res.body)).toEqual({
|
|
rates: [
|
|
{
|
|
coin: "BTC",
|
|
index: 0,
|
|
funding: "0.0000125",
|
|
premium: "0.00031774",
|
|
markPx: "14.3161",
|
|
oraclePx: "14.32",
|
|
openInterest: "688.11",
|
|
},
|
|
{
|
|
coin: "ETH",
|
|
index: 1,
|
|
funding: "-0.000001",
|
|
premium: "-0.00002",
|
|
markPx: "6.0436",
|
|
oraclePx: "6.0457",
|
|
openInterest: "1882.55",
|
|
},
|
|
],
|
|
source: "hyperliquid-info-meta-and-asset-ctxs",
|
|
fetchedAt: "2026-05-01T12:00:00.000Z",
|
|
});
|
|
});
|
|
|
|
it("returns status without fetch and rejects non-GET methods as execution-disabled", async () => {
|
|
const statusRes = responseRecorder();
|
|
await expect(
|
|
handleHyperliquidRoute(
|
|
{} as http.IncomingMessage,
|
|
statusRes as unknown as http.ServerResponse,
|
|
"/api/hyperliquid/status",
|
|
"GET",
|
|
{ env: {} as NodeJS.ProcessEnv },
|
|
),
|
|
).resolves.toBe(true);
|
|
|
|
expect(statusRes.status).toBe(200);
|
|
expect(JSON.parse(statusRes.body)).toMatchObject({
|
|
publicReadReady: true,
|
|
signerReady: false,
|
|
executionReady: false,
|
|
credentialMode: "none",
|
|
readiness: {
|
|
accountReads: false,
|
|
execution: false,
|
|
},
|
|
});
|
|
|
|
const postRes = responseRecorder();
|
|
await expect(
|
|
handleHyperliquidRoute(
|
|
{} as http.IncomingMessage,
|
|
postRes as unknown as http.ServerResponse,
|
|
"/api/hyperliquid/orders",
|
|
"POST",
|
|
{ env: {} as NodeJS.ProcessEnv },
|
|
),
|
|
).resolves.toBe(true);
|
|
expect(postRes.status).toBe(501);
|
|
expect(JSON.parse(postRes.body)).toMatchObject({
|
|
executionReady: false,
|
|
credentialMode: "none",
|
|
});
|
|
});
|
|
|
|
it("returns blocked positions when no account is configured and false for unrelated routes", async () => {
|
|
const res = responseRecorder();
|
|
|
|
await expect(
|
|
handleHyperliquidRoute(
|
|
{} as http.IncomingMessage,
|
|
res as unknown as http.ServerResponse,
|
|
"/api/hyperliquid/positions",
|
|
"GET",
|
|
{
|
|
env: {} as NodeJS.ProcessEnv,
|
|
fetchImpl: vi.fn() as unknown as HyperliquidFetch,
|
|
},
|
|
),
|
|
).resolves.toBe(true);
|
|
expect(res.status).toBe(200);
|
|
expect(JSON.parse(res.body)).toMatchObject({
|
|
accountAddress: null,
|
|
positions: [],
|
|
fetchedAt: null,
|
|
});
|
|
|
|
await expect(
|
|
handleHyperliquidRoute(
|
|
{} as http.IncomingMessage,
|
|
responseRecorder() as unknown as http.ServerResponse,
|
|
"/api/not-hyperliquid",
|
|
"GET",
|
|
),
|
|
).resolves.toBe(false);
|
|
});
|
|
|
|
it("normalizes positions and orders through the info client and throws on malformed payloads", async () => {
|
|
const fetchImpl = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
Response.json({
|
|
assetPositions: [
|
|
{
|
|
position: {
|
|
coin: "ETH",
|
|
szi: "1.25",
|
|
entryPx: "3000",
|
|
// positionValue 4000 / size 1.25 => mark 3200, deliberately
|
|
// above the 3000 entry so the derived mark (not entry) drives
|
|
// the distance-to-liquidation calc.
|
|
positionValue: "4000",
|
|
unrealizedPnl: "12.5",
|
|
returnOnEquity: "0.05",
|
|
liquidationPx: "2400",
|
|
marginUsed: "1000",
|
|
leverage: { type: "cross", value: 3 },
|
|
},
|
|
},
|
|
],
|
|
marginSummary: {
|
|
accountValue: "5000",
|
|
totalNtlPos: "4000",
|
|
totalMarginUsed: "1000",
|
|
totalRawUsd: "5012.5",
|
|
},
|
|
withdrawable: "4000",
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
Response.json([
|
|
{
|
|
coin: "BTC",
|
|
side: "B",
|
|
limitPx: "65000",
|
|
sz: "0.1",
|
|
oid: 123,
|
|
timestamp: 171000,
|
|
reduceOnly: true,
|
|
orderType: "Limit",
|
|
tif: "Gtc",
|
|
},
|
|
]),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
Response.json({ notUniverse: [] }),
|
|
) as unknown as vi.MockedFunction<HyperliquidFetch>;
|
|
const client = createHyperliquidInfoClient({ fetchImpl });
|
|
const account = "0x0000000000000000000000000000000000000001";
|
|
|
|
await expect(client.getPositions(account)).resolves.toEqual({
|
|
positions: [
|
|
{
|
|
coin: "ETH",
|
|
size: "1.25",
|
|
entryPx: "3000",
|
|
positionValue: "4000",
|
|
unrealizedPnl: "12.5",
|
|
returnOnEquity: "0.05",
|
|
liquidationPx: "2400",
|
|
marginUsed: "1000",
|
|
leverageType: "cross",
|
|
leverageValue: 3,
|
|
// mark = 4000 / 1.25 = 3200; (3200 - 2400) / 3200 = 25% to liq.
|
|
markPx: "3200",
|
|
distanceToLiquidationPct: 25,
|
|
},
|
|
],
|
|
summary: {
|
|
accountValue: "5000",
|
|
totalNotionalPosition: "4000",
|
|
totalMarginUsed: "1000",
|
|
totalRawUsd: "5012.5",
|
|
withdrawable: "4000",
|
|
totalUnrealizedPnl: "12.50",
|
|
// effective leverage = totalNtlPos 4000 / accountValue 5000 = 0.8.
|
|
effectiveLeverage: 0.8,
|
|
},
|
|
});
|
|
await expect(client.getOpenOrders(account)).resolves.toEqual([
|
|
{
|
|
coin: "BTC",
|
|
side: "B",
|
|
limitPx: "65000",
|
|
size: "0.1",
|
|
oid: 123,
|
|
timestamp: 171000,
|
|
reduceOnly: true,
|
|
orderType: "Limit",
|
|
tif: "Gtc",
|
|
cloid: null,
|
|
},
|
|
]);
|
|
await expect(client.getMarkets()).rejects.toThrow(
|
|
"Hyperliquid meta response missing universe",
|
|
);
|
|
|
|
expect(fetchImpl).toHaveBeenNthCalledWith(
|
|
1,
|
|
"https://api.hyperliquid.xyz/info",
|
|
expect.objectContaining({
|
|
body: JSON.stringify({ type: "clearinghouseState", user: account }),
|
|
}),
|
|
);
|
|
expect(fetchImpl).toHaveBeenNthCalledWith(
|
|
2,
|
|
"https://api.hyperliquid.xyz/info",
|
|
expect.objectContaining({
|
|
body: JSON.stringify({ type: "openOrders", user: account }),
|
|
}),
|
|
);
|
|
});
|
|
});
|