feat(exchanges): add Ondo Perps exchange adapter

This commit is contained in:
discountry
2026-07-11 15:10:40 +08:00
parent 6b5f2542a4
commit 1f2c2150e0
20 changed files with 2181 additions and 4 deletions
+10
View File
@@ -81,6 +81,16 @@ describe("command parser", () => {
});
});
it("parses the Ondo Perps exchange alias", () => {
for (const exchange of ["ondoperps", "ondoperp"]) {
const command = parseCommandArgv(["exchange", "capabilities", "--exchange", exchange]);
expect(command).toMatchObject({
kind: "exchange-capabilities",
exchange: "ondoperps",
});
}
});
it("throws for unsupported option", () => {
expect(() => parseCommandArgv(["doctor", "--unknown"])).toThrow(CommandParseError);
});
+15
View File
@@ -56,4 +56,19 @@ describe("resolveSymbolFromEnv", () => {
expect(resolveSymbolFromEnv("binance")).toBe("ETHUSDT");
});
it("supports Ondo Perps symbol defaults when explicit exchange id is provided", () => {
delete process.env.EXCHANGE;
process.env.ONDOPERPS_SYMBOL = "NVDA-USD.P";
expect(resolveSymbolFromEnv("ondoperps")).toBe("NVDA-USD.P");
});
it("supports the legacy Ondo Perps exchange id and symbol prefix", () => {
delete process.env.EXCHANGE;
delete process.env.ONDOPERPS_SYMBOL;
process.env.ONDOPERP_SYMBOL = "ETH-USD.P";
expect(resolveSymbolFromEnv("ondoperp")).toBe("ETH-USD.P");
});
});
+23 -1
View File
@@ -42,7 +42,7 @@ const REQUIRED_ENV_BY_EXCHANGE: Record<SupportedExchangeId, Record<string, strin
},
lighter: {
LIGHTER_ACCOUNT_INDEX: "1",
LIGHTER_API_PRIVATE_KEY: "lighter-private-key",
LIGHTER_API_PRIVATE_KEY: "0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b",
LIGHTER_API_KEY_INDEX: "0",
},
backpack: {
@@ -64,6 +64,10 @@ const REQUIRED_ENV_BY_EXCHANGE: Record<SupportedExchangeId, Record<string, strin
BINANCE_API_KEY: "binance-key",
BINANCE_API_SECRET: "binance-secret",
},
ondoperps: {
ONDOPERPS_API_KEY_ID: "ondoKeyId_test",
ONDOPERPS_API_SECRET: "ondoApiSecret_test",
},
};
class RecorderAdapter implements ExchangeAdapter {
@@ -151,6 +155,7 @@ describe("exchange contract suite", () => {
for (const id of SUPPORTED_EXCHANGE_IDS) {
expect(output).toContain(id);
}
expect(parseCliArgs(["--exchange", "ondoperp"]).exchange).toBe("ondoperps");
spy.mockRestore();
});
@@ -258,4 +263,21 @@ describe("exchange contract suite", () => {
}
}
});
it("routes orders from adapters that still report the legacy ondoperp id", async () => {
delete process.env.EXCHANGE;
delete process.env.TRADE_EXCHANGE;
const adapter = new RecorderAdapter("ondoperps");
(adapter as unknown as { id: string }).id = "ondoperp";
await routeLimitOrder({
adapter,
symbol: "BTC-USD.P",
side: "BUY",
quantity: 0.01,
price: 100_000,
});
expect(adapter.lastCreateOrderParams?.type).toBe("LIMIT");
});
});
+41
View File
@@ -1,11 +1,13 @@
import { describe, expect, it, beforeEach, afterEach } from "vitest";
import { createExchangeAdapter, resolveExchangeId } from "../src/exchanges/create-adapter";
import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env";
import { AsterExchangeAdapter } from "../src/exchanges/aster/adapter";
import { GrvtExchangeAdapter } from "../src/exchanges/grvt/adapter";
import { BackpackExchangeAdapter } from "../src/exchanges/backpack/adapter";
import { ParadexExchangeAdapter } from "../src/exchanges/paradex/adapter";
import { StandxExchangeAdapter } from "../src/exchanges/standx/adapter";
import { BinanceExchangeAdapter } from "../src/exchanges/binance/adapter";
import { OndoperpsExchangeAdapter } from "../src/exchanges/ondoperps/adapter";
const ORIGINAL_ENV = { ...process.env };
@@ -34,6 +36,8 @@ describe("exchange factory", () => {
expect(resolveExchangeId("PaRaDeX")).toBe("paradex");
expect(resolveExchangeId("StandX")).toBe("standx");
expect(resolveExchangeId("BiNaNcE")).toBe("binance");
expect(resolveExchangeId("OndoPerps")).toBe("ondoperps");
expect(resolveExchangeId("OndoPerp")).toBe("ondoperps");
});
it("creates grvt adapter when EXCHANGE=grvt", () => {
@@ -92,4 +96,41 @@ describe("exchange factory", () => {
expect(adapter).toBeInstanceOf(BinanceExchangeAdapter);
expect(adapter.id).toBe("binance");
});
it("creates ondoperps adapter when EXCHANGE=ondoperps", () => {
process.env.EXCHANGE = "ondoperps";
process.env.ONDOPERPS_API_KEY_ID = "ondoKeyId_test";
process.env.ONDOPERPS_API_SECRET = "ondoApiSecret_test";
process.env.ONDOPERPS_SYMBOL = "XAU-USD.P";
const adapter = createExchangeAdapter({ symbol: "XAU-USD.P" });
expect(adapter).toBeInstanceOf(OndoperpsExchangeAdapter);
expect(adapter.id).toBe("ondoperps");
});
it("accepts the legacy ondoperp id and environment prefix", () => {
delete process.env.ONDOPERPS_API_KEY_ID;
delete process.env.ONDOPERPS_API_SECRET;
delete process.env.ONDOPERPS_SYMBOL;
process.env.ONDOPERP_API_KEY_ID = "ondoKeyId_legacy";
process.env.ONDOPERP_API_SECRET = "ondoApiSecret_legacy";
process.env.ONDOPERP_SYMBOL = "ETH-USD.P";
const adapter = buildAdapterFromEnv({ exchangeId: "ondoperp", symbol: "BTC-USD.P" });
expect(adapter).toBeInstanceOf(OndoperpsExchangeAdapter);
expect(adapter.id).toBe("ondoperps");
});
it("accepts the legacy ondoperp factory option", () => {
const adapter = createExchangeAdapter({
exchange: "ondoperp",
symbol: "BTC-USD.P",
ondoperp: {
apiKeyId: "ondoKeyId_legacy",
apiSecret: "ondoApiSecret_legacy",
},
});
expect(adapter).toBeInstanceOf(OndoperpsExchangeAdapter);
expect(adapter.id).toBe("ondoperps");
});
});
+208
View File
@@ -0,0 +1,208 @@
import { describe, expect, it, vi } from "vitest";
import {
buildOndoperpsAuthHeaders,
createOndoperpsWsSignature,
mapOndoperpsAccountSnapshot,
mapOndoperpsOrder,
normalizeOndoperpsSymbol,
OndoperpsGateway,
} from "../src/exchanges/ondoperps/gateway";
describe("Ondo Perps gateway", () => {
it("normalizes common symbols to the official market format", () => {
expect(normalizeOndoperpsSymbol("")).toBe("BTC-USD.P");
expect(normalizeOndoperpsSymbol("xau")).toBe("XAU-USD.P");
expect(normalizeOndoperpsSymbol("NVDA/USD")).toBe("NVDA-USD.P");
expect(normalizeOndoperpsSymbol("AAPL-USD.P")).toBe("AAPL-USD.P");
});
it("builds REST and WebSocket HMAC signatures from the documented payload", () => {
const body = JSON.stringify({ market: "XAU-USD.P", side: "buy", size: "1" });
const headers = buildOndoperpsAuthHeaders({
apiKeyId: "ondoKeyId_test",
apiSecret: "ondoApiSecret_secret",
timestamp: "1700000000000",
method: "POST",
requestPath: "/v1/perps/orders?market=XAU-USD.P",
body,
});
expect(headers).toEqual({
"ONDO-KEY-ID": "ondoKeyId_test",
"ONDO-TIMESTAMP": "1700000000000",
"ONDO-SIGN": "3cf0628da79a19225d79fd17ab4767ee1ecae0219a52e1b1fc1b0c1a8bc7a880",
});
expect(createOndoperpsWsSignature("ondoApiSecret_secret", "1700000000000")).toBe(
"eb87f259c41c7cba728b1ce5baf0a385e6e5c1b1031424d73206cacbaebea742",
);
expect(createOndoperpsWsSignature("ondoApiSecret_secret", "1700000000000", true)).toBe(
"46ca7822ee8444eccd96feaa596e30072c8428aa8ec2cedbe0fdc6744bc21ec7",
);
});
it("maps Ondo order fields into the shared exchange contract", () => {
const order = mapOndoperpsOrder(
{
orderId: "order-1",
clientOrderId: "client-1",
side: "sell",
price: "201.25",
size: "2",
market: "NVDA-USD.P",
filledSize: "0.5",
filledCost: "100.625",
status: "open",
createdAt: "2026-07-11T00:00:00Z",
type: "limit",
timeInForce: "GTC",
reduceOnly: true,
},
"NVDA-USD.P",
);
expect(order).toMatchObject({
orderId: "order-1",
clientOrderId: "client-1",
symbol: "NVDA-USD.P",
side: "SELL",
type: "LIMIT",
status: "NEW",
origQty: "2",
executedQty: "0.5",
avgPrice: "201.25",
reduceOnly: true,
});
});
it("creates post-only orders with signed REST headers", async () => {
const requests: Array<{ url: string; init?: RequestInit }> = [];
const fetchFn = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = String(input);
requests.push({ url, init });
if (url.endsWith("/v1/markets")) {
return new Response(JSON.stringify({
success: true,
result: {
perps: {
tradingPairs: [
{ market: "XAU-USD.P", baseIncrement: "0.01", quoteIncrement: "0.1" },
],
},
},
}));
}
return new Response(JSON.stringify({
success: true,
result: {
orderId: "order-2",
clientOrderId: "client-2",
side: "buy",
price: "2500",
size: "0.5",
market: "XAU-USD.P",
filledSize: "0",
status: "open",
createdAt: "2026-07-11T00:00:00Z",
type: "limit",
timeInForce: "GTC",
reduceOnly: false,
},
}));
});
const fakeWebSocket = {
readyState: 0,
addEventListener: vi.fn(),
send: vi.fn(),
} as unknown as WebSocket;
const gateway = new OndoperpsGateway({
apiKeyId: "ondoKeyId_test",
apiSecret: "ondoApiSecret_secret",
symbol: "XAU-USD.P",
fetchFn: fetchFn as unknown as typeof fetch,
webSocketFactory: () => fakeWebSocket,
now: () => 1_700_000_000_000,
});
const order = await gateway.createOrder({
symbol: "XAU-USD.P",
side: "BUY",
type: "LIMIT",
quantity: 0.5,
price: 2500,
timeInForce: "GTX",
clientOrderId: "client-2",
});
const request = requests[1];
expect(request?.url).toBe("https://api.ondoperps.xyz/v1/perps/orders");
expect(JSON.parse(String(request?.init?.body))).toMatchObject({
market: "XAU-USD.P",
type: "limit",
side: "buy",
size: "0.5",
price: "2500",
timeInForce: "GTC",
postOnly: true,
clientOrderId: "client-2",
});
expect(request?.init?.headers).toMatchObject({
"ONDO-KEY-ID": "ondoKeyId_test",
"ONDO-TIMESTAMP": "1700000000000",
});
expect(order).toMatchObject({ orderId: "order-2", status: "NEW", type: "LIMIT" });
});
it("maps short positions and USDC margin fields into an account snapshot", () => {
const snapshot = mapOndoperpsAccountSnapshot(
{
walletBalance: "5000",
realizedPnl: "10",
unrealizedPnl: "-25",
marginBalance: "4975",
usedMargin: "1000",
availableMargin: "3975",
withdrawableMargin: "3975",
maintenanceMarginRequirement: "50",
totalMaintenanceMargin: "80",
marginRatio: "0.016",
leverage: "1.5",
underLiquidation: false,
totalFundingPayments: "-1",
totalTradingFees: "2",
totalPnL: "-18",
},
[
{
market: "NVDA-USD.P",
direction: "short",
netQuantity: "3",
averageEntryPrice: "200",
usedMargin: "1000",
unrealizedPnl: "-25",
markPrice: "208.33",
liquidationPrice: "260",
bankruptcyPrice: "275",
maintenanceMargin: "50",
notionalValue: "625",
leverage: "1.5",
netFundingSinceNeutral: "-1",
returnOnEquity: "-0.025",
},
],
"NVDA-USD.P",
"NVDA-USD.P",
1234,
);
expect(snapshot.totalWalletBalance).toBe("5000");
expect(snapshot.availableBalance).toBe("3975");
expect(snapshot.assets[0]?.asset).toBe("USDC");
expect(snapshot.positions[0]).toMatchObject({
symbol: "NVDA-USD.P",
positionAmt: "-3",
entryPrice: "200",
markPrice: "208.33",
positionSide: "BOTH",
});
});
});