perp-cli 0.14.0 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/arb/sizing.js +29 -7
- package/dist/bot/conditions.d.ts +5 -1
- package/dist/bot/conditions.js +16 -11
- package/dist/bot/engine.d.ts +12 -0
- package/dist/bot/engine.js +55 -44
- package/dist/bot/strategies/claude-agent.js +2 -2
- package/dist/bot/strategies/funding-arb-strategy.js +16 -20
- package/dist/bot/strategies/funding-arb-utils.d.ts +19 -0
- package/dist/bot/strategies/funding-arb-utils.js +44 -0
- package/dist/bot/strategies/funding-auto.js +61 -37
- package/dist/bot/strategies/grid-strategy.d.ts +2 -0
- package/dist/bot/strategies/grid-strategy.js +21 -2
- package/dist/bot/strategies/spot-perp-arb-strategy.js +28 -20
- package/dist/bot/tui/BotDashboard.d.ts +13 -2
- package/dist/bot/tui/BotDashboard.js +8 -3
- package/dist/bridge-engine.d.ts +2 -0
- package/dist/bridge-engine.js +68 -31
- package/dist/cli-program.js +19 -2
- package/dist/commands/account.js +2 -2
- package/dist/commands/arb/index.d.ts +20 -0
- package/dist/commands/arb/index.js +121 -36
- package/dist/commands/arb-auto.d.ts +36 -0
- package/dist/commands/arb-auto.js +202 -76
- package/dist/commands/arb.js +51 -35
- package/dist/commands/bot.d.ts +6 -0
- package/dist/commands/bot.js +55 -45
- package/dist/commands/funds-onchain.d.ts +1 -1
- package/dist/commands/funds-onchain.js +12 -49
- package/dist/commands/funds.js +51 -52
- package/dist/commands/history.js +165 -62
- package/dist/commands/portfolio.d.ts +5 -0
- package/dist/commands/portfolio.js +58 -20
- package/dist/commands/rebalance.js +46 -10
- package/dist/commands/settings.js +2 -1
- package/dist/commands/trade.js +8 -4
- package/dist/commands/wallet.js +74 -155
- package/dist/cross-chain-margin.d.ts +29 -2
- package/dist/cross-chain-margin.js +30 -4
- package/dist/dashboard/server.d.ts +45 -8
- package/dist/dashboard/server.js +113 -42
- package/dist/dashboard/ui.js +46 -27
- package/dist/dashboard/ws-feeds.d.ts +25 -3
- package/dist/dashboard/ws-feeds.js +219 -82
- package/dist/dex-asset-map.d.ts +23 -4
- package/dist/dex-asset-map.js +39 -13
- package/dist/exchanges/arcus/adapter.js +33 -6
- package/dist/exchanges/arcus/public.d.ts +6 -0
- package/dist/exchanges/arcus/public.js +12 -4
- package/dist/exchanges/aster.d.ts +33 -0
- package/dist/exchanges/aster.js +162 -55
- package/dist/exchanges/hyperliquid.d.ts +30 -0
- package/dist/exchanges/hyperliquid.js +159 -34
- package/dist/exchanges/lighter.d.ts +1 -1
- package/dist/exchanges/lighter.js +11 -9
- package/dist/exchanges/ondo/adapter.d.ts +4 -18
- package/dist/exchanges/ondo/adapter.js +5 -29
- package/dist/exchanges/ondo/funds.d.ts +1 -29
- package/dist/exchanges/ondo/funds.js +2 -94
- package/dist/exchanges/pacifica.d.ts +12 -0
- package/dist/exchanges/pacifica.js +78 -35
- package/dist/exchanges/qfex/adapter.js +4 -1
- package/dist/exchanges/venues/ondo.d.ts +0 -5
- package/dist/exchanges/venues/ondo.js +2 -6
- package/dist/exchanges/venues/qfex.d.ts +11 -0
- package/dist/exchanges/venues/qfex.js +10 -2
- package/dist/mcp-server.js +2 -2
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- package/dist/rebalance.d.ts +20 -1
- package/dist/rebalance.js +31 -5
- package/dist/strategies/funding-arb.js +38 -7
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +20 -3
- package/skills/perp-cli/references/agent-operations.md +2 -2
- package/skills/perp-cli/references/commands.md +12 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LocalEvmSigner } from "../signer/index.js";
|
|
2
2
|
import { DEFINITE_ORDER_FAILURE_CODES, PerpError, classifyError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure } from "../errors.js";
|
|
3
3
|
import { parseFiniteVenueNumber, venueRateField } from "../utils/numeric.js";
|
|
4
|
-
import { alignProtectionPrice, floorSize, formatComputedPrice, slippagePrice } from "./precision.js";
|
|
4
|
+
import { alignProtectionPrice, floorSize, formatComputedPrice, slippagePrice, subDecimal } from "./precision.js";
|
|
5
5
|
import { unsupported } from "./trading-v2.js";
|
|
6
6
|
import { buildHlUserSignedAction, hlUserSignedNonceField, signHlL1Action, signHlUserSignedAction } from "./hl-signing.js";
|
|
7
7
|
import { HL_CLOID_RE } from "./venues/hyperliquid.js";
|
|
@@ -339,6 +339,29 @@ export function hlL1ActionRejection(type, e) {
|
|
|
339
339
|
remediation: "The venue refused the whole request and executed nothing; fix the cause named in the venue message above before sending it again",
|
|
340
340
|
});
|
|
341
341
|
}
|
|
342
|
+
/** POST /info userFunding page size (docs: at most 500 rows per response). */
|
|
343
|
+
const HL_FUNDING_PAGE = 500;
|
|
344
|
+
/** 7 days of hourly funding = 168 rows per coin; 20 pages = 10 000 rows (~60 coins). */
|
|
345
|
+
const HL_FUNDING_MAX_PAGES = 20;
|
|
346
|
+
/** A required field of an HL info row as a string; missing / empty → EXCHANGE_ERROR. */
|
|
347
|
+
function hlField(row, key, endpoint) {
|
|
348
|
+
const v = row[key];
|
|
349
|
+
if ((typeof v === "string" && v !== "") || (typeof v === "number" && Number.isFinite(v)))
|
|
350
|
+
return String(v);
|
|
351
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid ${endpoint} row without a readable ${key}: ${JSON.stringify(row).slice(0, 160)}`, { exchange: "hyperliquid" });
|
|
352
|
+
}
|
|
353
|
+
/** historicalOrders status precedence on equal statusTimestamp: open < triggered < any terminal status. */
|
|
354
|
+
function hlStatusRank(status) {
|
|
355
|
+
return status === "open" ? 0 : status === "triggered" ? 1 : 2;
|
|
356
|
+
}
|
|
357
|
+
/** HL side "B" (bid) → buy, "A" (ask) → sell; anything else → EXCHANGE_ERROR. */
|
|
358
|
+
function hlSide(v, endpoint) {
|
|
359
|
+
if (v === "B")
|
|
360
|
+
return "buy";
|
|
361
|
+
if (v === "A")
|
|
362
|
+
return "sell";
|
|
363
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid ${endpoint} row with unknown side ${JSON.stringify(v)}`, { exchange: "hyperliquid" });
|
|
364
|
+
}
|
|
342
365
|
export class HyperliquidAdapter {
|
|
343
366
|
name = "hyperliquid";
|
|
344
367
|
chain = "evm";
|
|
@@ -1774,48 +1797,150 @@ export class HyperliquidAdapter {
|
|
|
1774
1797
|
trades: Number(c.n ?? 0),
|
|
1775
1798
|
}));
|
|
1776
1799
|
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Order history: POST /info {type:"historicalOrders"} ("Retrieve a user's historical orders",
|
|
1802
|
+
* at most 2000 most recent; live 2026-10-13: newest first, each order listed once per status —
|
|
1803
|
+
* e.g. "open" then "filled" — with equal statusTimestamp possible). One row per oid with its
|
|
1804
|
+
* final status (latest statusTimestamp; on a tie a non-"open" status wins). Includes canceled /
|
|
1805
|
+
* rejected orders (the previous userFills read showed fills only). Unreadable rows → EXCHANGE_ERROR.
|
|
1806
|
+
* The info endpoint is account-wide; rows are scoped to this adapter (see _inHistoryScope).
|
|
1807
|
+
*/
|
|
1777
1808
|
async getOrderHistory(limit = 30) {
|
|
1778
1809
|
this.ensureAddress();
|
|
1779
|
-
const
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1810
|
+
const rows = await this._infoPost({ type: "historicalOrders", user: this._address });
|
|
1811
|
+
if (!Array.isArray(rows)) {
|
|
1812
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid historicalOrders returned no list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "hyperliquid" });
|
|
1813
|
+
}
|
|
1814
|
+
await this._historyScopeReady();
|
|
1815
|
+
const latest = new Map();
|
|
1816
|
+
for (const row of rows) {
|
|
1817
|
+
const o = row.order;
|
|
1818
|
+
const at = Number(row.statusTimestamp);
|
|
1819
|
+
if (o === undefined || o === null || typeof row.status !== "string" || !Number.isFinite(at) || (typeof o.oid !== "number" && typeof o.oid !== "string")) {
|
|
1820
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid historicalOrders row without order / oid / status / statusTimestamp: ${JSON.stringify(row).slice(0, 160)}`, { exchange: "hyperliquid" });
|
|
1821
|
+
}
|
|
1822
|
+
if (!this._inHistoryScope(hlField(o, "coin", "historicalOrders")))
|
|
1823
|
+
continue;
|
|
1824
|
+
const key = String(o.oid);
|
|
1825
|
+
const prev = latest.get(key);
|
|
1826
|
+
if (prev === undefined || at > prev.at || (at === prev.at && hlStatusRank(row.status) > hlStatusRank(prev.status))) {
|
|
1827
|
+
latest.set(key, { at, status: row.status, o });
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return [...latest.values()].sort((x, y) => y.at - x.at).slice(0, limit).map(({ status, o }) => {
|
|
1831
|
+
const origSz = hlField(o, "origSz", "historicalOrders");
|
|
1832
|
+
const remaining = hlField(o, "sz", "historicalOrders");
|
|
1833
|
+
const triggerPx = Number(o.triggerPx);
|
|
1834
|
+
return {
|
|
1835
|
+
orderId: String(o.oid),
|
|
1836
|
+
symbol: this._displaySymbol(hlField(o, "coin", "historicalOrders")),
|
|
1837
|
+
side: hlSide(o.side, "historicalOrders"),
|
|
1838
|
+
price: hlField(o, "limitPx", "historicalOrders"),
|
|
1839
|
+
size: origSz,
|
|
1840
|
+
filled: subDecimal(origSz, remaining),
|
|
1841
|
+
status,
|
|
1842
|
+
type: hlField(o, "orderType", "historicalOrders"),
|
|
1843
|
+
triggerPrice: o.isTrigger === true && Number.isFinite(triggerPx) && triggerPx > 0 ? String(o.triggerPx) : null,
|
|
1844
|
+
};
|
|
1845
|
+
});
|
|
1791
1846
|
}
|
|
1847
|
+
/** Fills: POST /info {type:"userFills"} (at most 2000 most recent, newest first — live 2026-10-13). */
|
|
1792
1848
|
async getTradeHistory(limit = 30) {
|
|
1793
1849
|
this.ensureAddress();
|
|
1794
1850
|
const fills = await this._infoPost({ type: "userFills", user: this._address });
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1851
|
+
if (!Array.isArray(fills)) {
|
|
1852
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid userFills returned no list (${JSON.stringify(fills).slice(0, 120)})`, { exchange: "hyperliquid" });
|
|
1853
|
+
}
|
|
1854
|
+
await this._historyScopeReady();
|
|
1855
|
+
return fills.filter((f) => this._inHistoryScope(hlField(f, "coin", "userFills"))).slice(0, limit).map((f) => {
|
|
1856
|
+
const time = Number(hlField(f, "time", "userFills"));
|
|
1857
|
+
if (!Number.isFinite(time))
|
|
1858
|
+
throw new PerpError("EXCHANGE_ERROR", "Hyperliquid userFills row with a non-numeric time", { exchange: "hyperliquid" });
|
|
1859
|
+
return {
|
|
1860
|
+
time,
|
|
1861
|
+
symbol: this._displaySymbol(hlField(f, "coin", "userFills")),
|
|
1862
|
+
side: hlSide(f.side, "userFills"),
|
|
1863
|
+
price: hlField(f, "px", "userFills"),
|
|
1864
|
+
size: hlField(f, "sz", "userFills"),
|
|
1865
|
+
fee: hlField(f, "fee", "userFills"),
|
|
1866
|
+
};
|
|
1867
|
+
});
|
|
1803
1868
|
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Funding payments in this adapter's scope (see _inHistoryScope), newest first, at most `limit`.
|
|
1871
|
+
* Reads the last 24 h first and widens to 7 days only when that holds fewer than `limit` rows
|
|
1872
|
+
* (POST /info userFunding weighs per row returned; bots poll it).
|
|
1873
|
+
*/
|
|
1804
1874
|
async getFundingPayments(limit = 200) {
|
|
1805
1875
|
this.ensureAddress();
|
|
1806
|
-
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1876
|
+
await this._historyScopeReady();
|
|
1877
|
+
const now = Date.now();
|
|
1878
|
+
let rows = await this._fundingSince(now - 24 * 60 * 60 * 1000);
|
|
1879
|
+
if (rows.length < limit)
|
|
1880
|
+
rows = await this._fundingSince(now - 7 * 24 * 60 * 60 * 1000);
|
|
1881
|
+
return rows.sort((a, b) => b.time - a.time).slice(0, limit);
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Every userFunding row since `startTime`. The endpoint returns rows OLDEST first, at most 500
|
|
1885
|
+
* per response (live 2026-10-13), and every coin of a funding hour shares one millisecond — so
|
|
1886
|
+
* pages continue from the last row's time INCLUSIVE and rows are deduped by time + coin (funding
|
|
1887
|
+
* rows carry a zero hash). A full page that adds nothing new, or more than HL_FUNDING_MAX_PAGES
|
|
1888
|
+
* pages → EXCHANGE_ERROR (never a list with payments silently missing).
|
|
1889
|
+
*/
|
|
1890
|
+
async _fundingSince(startTime) {
|
|
1891
|
+
const out = [];
|
|
1892
|
+
const seen = new Set();
|
|
1893
|
+
let from = startTime;
|
|
1894
|
+
for (let page = 0;; page++) {
|
|
1895
|
+
if (page >= HL_FUNDING_MAX_PAGES) {
|
|
1896
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid funding history needs more than ${HL_FUNDING_MAX_PAGES} pages — the result would be incomplete`, { exchange: "hyperliquid" });
|
|
1897
|
+
}
|
|
1898
|
+
const rows = await this._infoPost({ type: "userFunding", user: this._address, startTime: from });
|
|
1899
|
+
if (!Array.isArray(rows)) {
|
|
1900
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid userFunding returned no list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "hyperliquid" });
|
|
1901
|
+
}
|
|
1902
|
+
let last = -Infinity;
|
|
1903
|
+
let fresh = 0;
|
|
1904
|
+
for (const h of rows) {
|
|
1905
|
+
const time = Number(h.time);
|
|
1906
|
+
const delta = h.delta;
|
|
1907
|
+
if (!Number.isFinite(time) || delta === undefined || delta === null || typeof delta.coin !== "string" || delta.coin === "" || typeof delta.usdc !== "string" || delta.usdc === "") {
|
|
1908
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid userFunding row without time / delta.coin / delta.usdc: ${JSON.stringify(h).slice(0, 160)}`, { exchange: "hyperliquid" });
|
|
1909
|
+
}
|
|
1910
|
+
last = Math.max(last, time);
|
|
1911
|
+
const key = `${time}|${delta.coin}`;
|
|
1912
|
+
if (seen.has(key))
|
|
1913
|
+
continue;
|
|
1914
|
+
seen.add(key);
|
|
1915
|
+
fresh++;
|
|
1916
|
+
if (this._inHistoryScope(delta.coin))
|
|
1917
|
+
out.push({ time, symbol: this._displaySymbol(delta.coin), payment: delta.usdc });
|
|
1918
|
+
}
|
|
1919
|
+
if (rows.length < HL_FUNDING_PAGE)
|
|
1920
|
+
break;
|
|
1921
|
+
if (fresh === 0) {
|
|
1922
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid returned ${HL_FUNDING_PAGE}+ funding rows at one millisecond (${last}) — paging cannot advance`, { exchange: "hyperliquid" });
|
|
1923
|
+
}
|
|
1924
|
+
from = last;
|
|
1925
|
+
}
|
|
1926
|
+
return out;
|
|
1927
|
+
}
|
|
1928
|
+
/** The asset map backs _displaySymbol / _inHistoryScope; load it before mapping history rows. */
|
|
1929
|
+
async _historyScopeReady() {
|
|
1930
|
+
if (this._assetMap.size === 0)
|
|
1931
|
+
await this._loadAssetMap();
|
|
1932
|
+
if (this._assetMapError)
|
|
1933
|
+
throw this._assetMapError;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Whether an account-wide history row (historicalOrders / userFills / userFunding carry every
|
|
1937
|
+
* dex, spot "@n" / "PAIR/USDC" and outcome "#n" coins) belongs to this adapter: a HIP-3 dex
|
|
1938
|
+
* adapter keeps "<dex>:…" coins, the native adapter keeps native perp coins only.
|
|
1939
|
+
*/
|
|
1940
|
+
_inHistoryScope(coin) {
|
|
1941
|
+
if (this._dex)
|
|
1942
|
+
return coin.startsWith(`${this._dex}:`);
|
|
1943
|
+
return !/[:@#/]/.test(coin);
|
|
1819
1944
|
}
|
|
1820
1945
|
async getFundingHistory(symbol, limit = 10) {
|
|
1821
1946
|
const now = Date.now();
|
|
@@ -154,7 +154,7 @@ export declare function lighterTakerFilledBase(eventInfo: unknown, sentBaseAmoun
|
|
|
154
154
|
* A required open-order row field as a string (Rule #2): missing / empty / non-finite numeric →
|
|
155
155
|
* EXCHANGE_ERROR, never listed as price / size "0" or status "open".
|
|
156
156
|
*/
|
|
157
|
-
export declare function lighterRowField(o: Record<string, unknown>, key: string, venue: string): string;
|
|
157
|
+
export declare function lighterRowField(o: Record<string, unknown>, key: string, venue: string, what?: "open order" | "order history"): string;
|
|
158
158
|
/** What identifies OUR accountOrders row besides the client order index: the market sent to and the send time. */
|
|
159
159
|
export interface LighterRowMatch {
|
|
160
160
|
marketIndex: number;
|
|
@@ -250,12 +250,12 @@ export function lighterTakerFilledBase(eventInfo, sentBaseAmount) {
|
|
|
250
250
|
* A required open-order row field as a string (Rule #2): missing / empty / non-finite numeric →
|
|
251
251
|
* EXCHANGE_ERROR, never listed as price / size "0" or status "open".
|
|
252
252
|
*/
|
|
253
|
-
export function lighterRowField(o, key, venue) {
|
|
253
|
+
export function lighterRowField(o, key, venue, what = "open order") {
|
|
254
254
|
const v = o[key];
|
|
255
255
|
if ((typeof v === "string" && v.trim() !== "") || (typeof v === "number" && Number.isFinite(v)))
|
|
256
256
|
return String(v);
|
|
257
|
-
throw new PerpError("EXCHANGE_ERROR", `${venue}
|
|
258
|
-
exchange: venue, remediation: `Retry: perp -e ${venue} account orders`,
|
|
257
|
+
throw new PerpError("EXCHANGE_ERROR", `${venue} ${what} ${lighterOrderIdOf(o) || "(no id)"}: row has no ${key} (got ${JSON.stringify(v ?? null)})`, {
|
|
258
|
+
exchange: venue, remediation: `Retry: perp -e ${venue} ${what === "open order" ? "account orders" : "account history"}`,
|
|
259
259
|
});
|
|
260
260
|
}
|
|
261
261
|
/** `iocOutcomeOf` parser shared by LighterAdapter and LighterSpotAdapter. */
|
|
@@ -1438,7 +1438,7 @@ export class LighterAdapter {
|
|
|
1438
1438
|
size: lighterRowField(o, "initial_base_amount", this.name),
|
|
1439
1439
|
filled: lighterRowField(o, "filled_base_amount", this.name),
|
|
1440
1440
|
status: lighterRowField(o, "status", this.name),
|
|
1441
|
-
type:
|
|
1441
|
+
type: lighterRowField(o, "type", this.name),
|
|
1442
1442
|
triggerPrice: Number(o.trigger_price) > 0 ? String(o.trigger_price) : null,
|
|
1443
1443
|
};
|
|
1444
1444
|
});
|
|
@@ -2097,15 +2097,17 @@ export class LighterAdapter {
|
|
|
2097
2097
|
if (!this._address)
|
|
2098
2098
|
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
2099
2099
|
const orders = await this.historyRows("/accountInactiveOrders", "orders", { market_type: "perp" }, limit, true);
|
|
2100
|
+
// Every field is required: a missing price / size / status / type is an unreadable row, never "0" / "done" / "limit".
|
|
2101
|
+
const req = (o, k) => lighterRowField(o, k, this.name, "order history");
|
|
2100
2102
|
return orders.map((o) => ({
|
|
2101
2103
|
orderId: lighterOrderIdOf(o),
|
|
2102
2104
|
symbol: this.historySymbol(o.market_index, "order history", o),
|
|
2103
2105
|
side: this.sideOfIsAsk(o.is_ask, "order history", o),
|
|
2104
|
-
price:
|
|
2105
|
-
size:
|
|
2106
|
-
filled:
|
|
2107
|
-
status:
|
|
2108
|
-
type:
|
|
2106
|
+
price: req(o, "price"),
|
|
2107
|
+
size: req(o, "initial_base_amount"),
|
|
2108
|
+
filled: req(o, "filled_base_amount"),
|
|
2109
|
+
status: req(o, "status"),
|
|
2110
|
+
type: req(o, "type"),
|
|
2109
2111
|
triggerPrice: Number(o.trigger_price) > 0 ? String(o.trigger_price) : null,
|
|
2110
2112
|
}));
|
|
2111
2113
|
}
|
|
@@ -42,12 +42,6 @@ import type { VenueCreateContext, VenueDefinition } from "../venue.js";
|
|
|
42
42
|
import type { AmendRequest, CancelResult, LeverageResult, Network, OrderRef, OrderRequest, OrderResult, VenueMarket } from "../venue-types.js";
|
|
43
43
|
import { type BuilderFeeGate } from "../builder-fee.js";
|
|
44
44
|
import { OndoClient } from "./client.js";
|
|
45
|
-
interface OndoWithdrawal {
|
|
46
|
-
withdrawal_status: string;
|
|
47
|
-
withdrawal_id: string;
|
|
48
|
-
customer_withdrawal_id: string;
|
|
49
|
-
hash?: string;
|
|
50
|
-
}
|
|
51
45
|
export declare class OndoAdapter implements ExchangeAdapter, TradingV2 {
|
|
52
46
|
readonly name: "ondo";
|
|
53
47
|
readonly aliases: readonly ["ondo-perps"];
|
|
@@ -72,8 +66,6 @@ export declare class OndoAdapter implements ExchangeAdapter, TradingV2 {
|
|
|
72
66
|
get restClient(): OndoClient;
|
|
73
67
|
/** Account id from GET /v1/account — the `from.id`/`to.id` of withdrawals and spot↔perp transfers. */
|
|
74
68
|
accountId(): Promise<string>;
|
|
75
|
-
/** Withdrawal address book (GET /v1/wallet/address_book) — withdrawals go only to these addresses. */
|
|
76
|
-
addressBook(): Promise<string[]>;
|
|
77
69
|
private pairOf;
|
|
78
70
|
/**
|
|
79
71
|
* An order the venue returned as "pending" (not yet open / filled) is read back by id
|
|
@@ -152,15 +144,9 @@ export declare class OndoAdapter implements ExchangeAdapter, TradingV2 {
|
|
|
152
144
|
triggerType?: "mark" | "last" | "mid";
|
|
153
145
|
}): Promise<OrderResult>;
|
|
154
146
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
147
|
+
* Withdrawals are not available over the API (account owner, 2026-10-13): Ondo pays only
|
|
148
|
+
* address-book destinations and refuses API-key auth for adding one
|
|
149
|
+
* (`key_not_allowed_for_action`). Refused before any request — withdraw in the web app.
|
|
158
150
|
*/
|
|
159
|
-
withdraw(
|
|
160
|
-
assetId?: number;
|
|
161
|
-
routeType?: number;
|
|
162
|
-
}): Promise<OndoWithdrawal & {
|
|
163
|
-
withdrawalFeeUSD: string;
|
|
164
|
-
}>;
|
|
151
|
+
withdraw(_amount: string, _destination: string): Promise<never>;
|
|
165
152
|
}
|
|
166
|
-
export {};
|
|
@@ -41,7 +41,6 @@ import { alignProtectionPrice, floorSize, formatComputedPrice, requireOnTick, sl
|
|
|
41
41
|
import { unsupported, validateLegacyTif, validateOrderRequest } from "../trading-v2.js";
|
|
42
42
|
import { networkConfig, requireCapabilities } from "../venue.js";
|
|
43
43
|
import { parseOrderRef, toLegacyMarketInfo } from "../venue-types.js";
|
|
44
|
-
import { ONDO_WITHDRAW_NETWORK } from "../venues/ondo.js";
|
|
45
44
|
import { builderGateOf, builderRejectionOf, NO_BUILDER, requireFeeBps } from "../builder-fee.js";
|
|
46
45
|
import { loadOndoCredentials, ONDO_APPROVE_REMEDIATION } from "./agent.js";
|
|
47
46
|
import { OndoClient } from "./client.js";
|
|
@@ -217,10 +216,6 @@ export class OndoAdapter {
|
|
|
217
216
|
const account = await this.client.get("/v1/account", null);
|
|
218
217
|
return account.accountID;
|
|
219
218
|
}
|
|
220
|
-
/** Withdrawal address book (GET /v1/wallet/address_book) — withdrawals go only to these addresses. */
|
|
221
|
-
async addressBook() {
|
|
222
|
-
return (await import("./funds.js")).ondoAddressBook(this.client);
|
|
223
|
-
}
|
|
224
219
|
// ── Helpers ──
|
|
225
220
|
async pairOf(symbol) {
|
|
226
221
|
const market = ondoVenueSymbol(symbol);
|
|
@@ -806,30 +801,11 @@ export class OndoAdapter {
|
|
|
806
801
|
}
|
|
807
802
|
// ── Withdraw (capabilities.WithdrawCapable) ──
|
|
808
803
|
/**
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
804
|
+
* Withdrawals are not available over the API (account owner, 2026-10-13): Ondo pays only
|
|
805
|
+
* address-book destinations and refuses API-key auth for adding one
|
|
806
|
+
* (`key_not_allowed_for_action`). Refused before any request — withdraw in the web app.
|
|
812
807
|
*/
|
|
813
|
-
async withdraw(
|
|
814
|
-
|
|
815
|
-
throw new PerpError("INVALID_PARAMS", "Ondo Perps withdraw takes no assetId/routeType (USDC on Ethereum only)");
|
|
816
|
-
}
|
|
817
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(destination)) {
|
|
818
|
-
throw new PerpError("INVALID_PARAMS", `Ondo Perps withdraw destination must be an Ethereum address (got "${destination}")`);
|
|
819
|
-
}
|
|
820
|
-
if (!/^\d+(\.\d+)?$/.test(amount) || !isPositive(amount)) {
|
|
821
|
-
throw new PerpError("INVALID_PARAMS", `Withdraw amount must be a positive decimal (got "${amount}")`);
|
|
822
|
-
}
|
|
823
|
-
// AccountInfo.withdrawalFeeUSD (required in the OpenAPI spec) is reported with the result.
|
|
824
|
-
const account = await this.client.get("/v1/account", null);
|
|
825
|
-
const res = await this.client.post("/v1/withdraw", {
|
|
826
|
-
customer_withdrawal_id: randomUUID(),
|
|
827
|
-
symbol: "USDC",
|
|
828
|
-
network: ONDO_WITHDRAW_NETWORK,
|
|
829
|
-
amount,
|
|
830
|
-
address: destination,
|
|
831
|
-
from: { id: account.accountID, wallet: "margin" },
|
|
832
|
-
});
|
|
833
|
-
return { ...res, withdrawalFeeUSD: account.withdrawalFeeUSD };
|
|
808
|
+
async withdraw(_amount, _destination) {
|
|
809
|
+
throw unsupported("Ondo Perps", "withdraw", "the Ondo Perps API does not allow withdrawals", "Withdraw from the Ondo Perps web app");
|
|
834
810
|
}
|
|
835
811
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ondo Perps funding (mainnet only)
|
|
2
|
+
* Ondo Perps funding (mainnet only). Withdrawals are web-app only (the API cannot withdraw).
|
|
3
3
|
*
|
|
4
4
|
* Deposit = a plain ERC-20 transfer of USDC to the account's own funding address
|
|
5
5
|
* (docs.ondoperps.xyz/funding-ondo-perps, api-reference/integration_guide "Deposit USDC").
|
|
@@ -13,12 +13,7 @@
|
|
|
13
13
|
* - With a probe amount the transfer is two-step: probe → wait until GET /v1/wallet/deposits
|
|
14
14
|
* shows it confirmed → remainder. A probe that is not credited within the window stops the
|
|
15
15
|
* flow (ORDER_PENDING with the probe tx hash); the remainder is NOT sent.
|
|
16
|
-
*
|
|
17
|
-
* Address book: withdrawals go only to addresses on the account's address book. An address is
|
|
18
|
-
* added by signing a SIWE challenge with the account's wallet
|
|
19
|
-
* (POST /v1/auth/erc-4361/address_book/get_challenge → personal_sign → …/complete_challenge).
|
|
20
16
|
*/
|
|
21
|
-
import { PerpError } from "../../errors.js";
|
|
22
17
|
import { type TxSigner } from "../../evm-master-tx.js";
|
|
23
18
|
import type { Network } from "../venue-types.js";
|
|
24
19
|
import { type OndoDepositChain } from "../venues/ondo.js";
|
|
@@ -26,7 +21,6 @@ import { type OndoDepositChain } from "../venues/ondo.js";
|
|
|
26
21
|
export interface OndoFundsClient {
|
|
27
22
|
get<T>(path: string, query: null): Promise<T>;
|
|
28
23
|
post<T>(path: string, body: unknown): Promise<T>;
|
|
29
|
-
postNoResult(path: string, body: unknown): Promise<void>;
|
|
30
24
|
}
|
|
31
25
|
export declare const ONDO_DEPOSIT_CREDIT_TIMEOUT_MS = 600000;
|
|
32
26
|
export declare const ONDO_DEPOSIT_CREDIT_POLL_MS = 10000;
|
|
@@ -98,25 +92,3 @@ export interface OndoDepositResult {
|
|
|
98
92
|
* remainder is never sent while the probe is uncredited (the ORDER_PENDING carries `sent`).
|
|
99
93
|
*/
|
|
100
94
|
export declare function executeOndoDeposit(plan: OndoDepositPlan, sign: TxSigner, readDeposits: () => Promise<unknown>, timing: PollTiming): Promise<OndoDepositResult>;
|
|
101
|
-
/** Withdrawal addresses on the account's address book (GET /v1/wallet/address_book). */
|
|
102
|
-
export declare function ondoAddressBook(client: OndoFundsClient): Promise<string[]>;
|
|
103
|
-
/** Refusal for a withdraw destination that is not on the address book (nothing was sent). */
|
|
104
|
-
export declare function ondoAddressNotOnBook(destination: string, book: readonly string[]): PerpError;
|
|
105
|
-
/** A challenge the master key will sign must be about this address book entry — never an arbitrary message. */
|
|
106
|
-
export declare function assertOndoAddressBookChallenge(message: string, wallet: string, withdrawalAddress: string, restUrl: string): void;
|
|
107
|
-
export interface OndoAddressBookAddResult {
|
|
108
|
-
address: string;
|
|
109
|
-
alreadyListed: boolean;
|
|
110
|
-
listed: boolean;
|
|
111
|
-
challengeId: string | null;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Add the signing wallet's own address to the address book: get_challenge → personal_sign
|
|
115
|
-
* (EIP-191) with the wallet → complete_challenge. Challenge chainId "1" (enum: 1 | 43114).
|
|
116
|
-
*/
|
|
117
|
-
export declare function addOndoAddressBookEntry(client: OndoFundsClient, p: {
|
|
118
|
-
wallet: string;
|
|
119
|
-
restUrl: string;
|
|
120
|
-
label: string | null;
|
|
121
|
-
signMessage: (message: string) => Promise<string>;
|
|
122
|
-
}): Promise<OndoAddressBookAddResult>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ondo Perps funding (mainnet only)
|
|
2
|
+
* Ondo Perps funding (mainnet only). Withdrawals are web-app only (the API cannot withdraw).
|
|
3
3
|
*
|
|
4
4
|
* Deposit = a plain ERC-20 transfer of USDC to the account's own funding address
|
|
5
5
|
* (docs.ondoperps.xyz/funding-ondo-perps, api-reference/integration_guide "Deposit USDC").
|
|
@@ -13,12 +13,8 @@
|
|
|
13
13
|
* - With a probe amount the transfer is two-step: probe → wait until GET /v1/wallet/deposits
|
|
14
14
|
* shows it confirmed → remainder. A probe that is not credited within the window stops the
|
|
15
15
|
* flow (ORDER_PENDING with the probe tx hash); the remainder is NOT sent.
|
|
16
|
-
*
|
|
17
|
-
* Address book: withdrawals go only to addresses on the account's address book. An address is
|
|
18
|
-
* added by signing a SIWE challenge with the account's wallet
|
|
19
|
-
* (POST /v1/auth/erc-4361/address_book/get_challenge → personal_sign → …/complete_challenge).
|
|
20
16
|
*/
|
|
21
|
-
import { formatUnits, parseUnits
|
|
17
|
+
import { formatUnits, parseUnits } from "ethers";
|
|
22
18
|
import { PerpError } from "../../errors.js";
|
|
23
19
|
import { ERC20, EVM_ADDRESS_RE, assertRpcChainId, erc20Balance, erc20Decimals, evmProvider, sameAddress, sendMasterTx, } from "../../evm-master-tx.js";
|
|
24
20
|
import { unsupported } from "../trading-v2.js";
|
|
@@ -200,91 +196,3 @@ export async function executeOndoDeposit(plan, sign, readDeposits, timing) {
|
|
|
200
196
|
}
|
|
201
197
|
return { transfers, depositAddress: plan.depositAddress };
|
|
202
198
|
}
|
|
203
|
-
// ── Address book ──
|
|
204
|
-
const ADDRESS_BOOK_UI = "add the address in the Ondo Perps web app (Withdraw → address book)";
|
|
205
|
-
/** Withdrawal addresses on the account's address book (GET /v1/wallet/address_book). */
|
|
206
|
-
export async function ondoAddressBook(client) {
|
|
207
|
-
const res = await client.get("/v1/wallet/address_book", null);
|
|
208
|
-
if (!Array.isArray(res.addressBook))
|
|
209
|
-
throw venueError("GET /v1/wallet/address_book returned no addressBook list");
|
|
210
|
-
return res.addressBook.map(e => {
|
|
211
|
-
if (typeof e?.withdrawalAddress !== "string")
|
|
212
|
-
throw venueError("address book entry has no withdrawalAddress");
|
|
213
|
-
return e.withdrawalAddress;
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
/** Refusal for a withdraw destination that is not on the address book (nothing was sent). */
|
|
217
|
-
export function ondoAddressNotOnBook(destination, book) {
|
|
218
|
-
return new PerpError("INVALID_PARAMS", `${destination} is not on the Ondo Perps withdrawal address book (${book.length === 0 ? "the book is empty" : `listed: ${book.join(", ")}`}) — nothing was sent`, {
|
|
219
|
-
exchange: "ondo",
|
|
220
|
-
remediation: `Ondo only withdraws to address-book entries. An entry is added by signing a SIWE challenge with the account's wallet (api-reference/auth/get-siwe-address-book-challenge + complete). For the master wallet's own address run: perp funds address-book add ondo ${destination} — otherwise ${ADDRESS_BOOK_UI}`,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
/** A challenge the master key will sign must be about this address book entry — never an arbitrary message. */
|
|
224
|
-
export function assertOndoAddressBookChallenge(message, wallet, withdrawalAddress, restUrl) {
|
|
225
|
-
const refuse = (why) => {
|
|
226
|
-
throw new PerpError("POLICY_DENIED", `Ondo address-book challenge refused: ${why}. Nothing was signed.`, {
|
|
227
|
-
exchange: "ondo", challenge: message.slice(0, 600), remediation: `If this message is expected, ${ADDRESS_BOOK_UI}`,
|
|
228
|
-
});
|
|
229
|
-
};
|
|
230
|
-
if (!message.toLowerCase().includes(withdrawalAddress.toLowerCase()))
|
|
231
|
-
refuse(`the message does not name the withdrawal address ${withdrawalAddress}`);
|
|
232
|
-
// The endpoint is POST /v1/auth/erc-4361/address_book/get_challenge — "Get a Sign-In with Ethereum
|
|
233
|
-
// challenge for adding a withdrawal address" (Ondo REST spec). ERC-4361 shape:
|
|
234
|
-
// "<domain> wants you to sign in with your Ethereum account:\n<address>\n…". Any other message
|
|
235
|
-
// is refused: the master key never signs free-form text.
|
|
236
|
-
const siwe = /^(\S+) wants you to sign in with your Ethereum account:\n(0x[0-9a-fA-F]{40})\n/.exec(message);
|
|
237
|
-
if (siwe === null)
|
|
238
|
-
return refuse("the message is not a Sign-In with Ethereum (ERC-4361) message");
|
|
239
|
-
const venueHost = new URL(restUrl).hostname.split(".").slice(-2).join(".");
|
|
240
|
-
const domain = siwe[1].replace(/^[a-z]+:\/\//, "").split(":")[0];
|
|
241
|
-
if (domain !== venueHost && !domain.endsWith(`.${venueHost}`))
|
|
242
|
-
refuse(`its domain "${siwe[1]}" is not ${venueHost}`);
|
|
243
|
-
if (!sameAddress(siwe[2], wallet))
|
|
244
|
-
refuse(`it is addressed to ${siwe[2]}, not the signing wallet ${wallet}`);
|
|
245
|
-
}
|
|
246
|
-
/** A venue refusal of the SIWE address-book endpoints, re-raised with the web-app route. */
|
|
247
|
-
function addressBookAuthError(e, step) {
|
|
248
|
-
if (!(e instanceof PerpError) || e.structured.code === "ORDER_PENDING")
|
|
249
|
-
return e;
|
|
250
|
-
return new PerpError(e.structured.code, `${e.message} — Ondo refused the address-book ${step} with API-key auth`, {
|
|
251
|
-
...e.structured.details, exchange: "ondo",
|
|
252
|
-
remediation: `These endpoints may need a wallet (SIWE/JWT) session instead of an API key: ${ADDRESS_BOOK_UI}`,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Add the signing wallet's own address to the address book: get_challenge → personal_sign
|
|
257
|
-
* (EIP-191) with the wallet → complete_challenge. Challenge chainId "1" (enum: 1 | 43114).
|
|
258
|
-
*/
|
|
259
|
-
export async function addOndoAddressBookEntry(client, p) {
|
|
260
|
-
const before = await ondoAddressBook(client);
|
|
261
|
-
if (before.some(a => sameAddress(a, p.wallet)))
|
|
262
|
-
return { address: p.wallet, alreadyListed: true, listed: true, challengeId: null };
|
|
263
|
-
let challenge;
|
|
264
|
-
try {
|
|
265
|
-
challenge = await client.post("/v1/auth/erc-4361/address_book/get_challenge", {
|
|
266
|
-
walletAddress: p.wallet, chainId: "1", withdrawalAddress: p.wallet,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
catch (e) {
|
|
270
|
-
throw addressBookAuthError(e, "challenge");
|
|
271
|
-
}
|
|
272
|
-
if (typeof challenge.id !== "string" || typeof challenge.message !== "string" || challenge.message === "") {
|
|
273
|
-
throw venueError("address_book/get_challenge returned no id / message");
|
|
274
|
-
}
|
|
275
|
-
assertOndoAddressBookChallenge(challenge.message, p.wallet, p.wallet, p.restUrl);
|
|
276
|
-
const signature = await p.signMessage(challenge.message);
|
|
277
|
-
if (!sameAddress(verifyMessage(challenge.message, signature), p.wallet)) {
|
|
278
|
-
throw new PerpError("SIGNATURE_FAILED", `The address-book challenge signature does not recover to ${p.wallet} — nothing was submitted`, { exchange: "ondo" });
|
|
279
|
-
}
|
|
280
|
-
try {
|
|
281
|
-
await client.postNoResult("/v1/auth/erc-4361/address_book/complete_challenge", {
|
|
282
|
-
id: challenge.id, signature, ...(p.label !== null ? { addressLabel: p.label } : {}),
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
catch (e) {
|
|
286
|
-
throw addressBookAuthError(e, "completion");
|
|
287
|
-
}
|
|
288
|
-
const after = await ondoAddressBook(client);
|
|
289
|
-
return { address: p.wallet, alreadyListed: false, listed: after.some(a => sameAddress(a, p.wallet)), challengeId: challenge.id };
|
|
290
|
-
}
|
|
@@ -375,7 +375,19 @@ export declare class PacificaAdapter implements ExchangeAdapter {
|
|
|
375
375
|
price: string | null;
|
|
376
376
|
}[]>;
|
|
377
377
|
getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
|
|
378
|
+
/**
|
|
379
|
+
* Recent orders, newest first (GET /orders/history; PacificaClient.get already unwraps the
|
|
380
|
+
* `{success, data}` envelope, so the list is the result itself). A non-list or a row without a
|
|
381
|
+
* readable field → EXCHANGE_ERROR, never an empty list or a placeholder value.
|
|
382
|
+
*/
|
|
378
383
|
getOrderHistory(limit?: number): Promise<ExchangeOrder[]>;
|
|
384
|
+
/**
|
|
385
|
+
* Recent fills, newest first (GET /trades/history; the client unwraps `{success, data}`).
|
|
386
|
+
* `side` is the position effect — open_long / close_short buy, open_short / close_long sell
|
|
387
|
+
* (live 2026-10-13: `"side":"close_short"` on a buy-to-close fill); bid / ask are accepted for
|
|
388
|
+
* rows that carry an order side. Anything else, a non-list or an unreadable field → EXCHANGE_ERROR.
|
|
389
|
+
*/
|
|
379
390
|
getTradeHistory(limit?: number): Promise<ExchangeTrade[]>;
|
|
391
|
+
/** Funding paid / received per position (GET /funding/history, `payout` signed). Strict like getTradeHistory. */
|
|
380
392
|
getFundingPayments(limit?: number): Promise<ExchangeFundingPayment[]>;
|
|
381
393
|
}
|