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.
Files changed (75) hide show
  1. package/dist/arb/sizing.js +29 -7
  2. package/dist/bot/conditions.d.ts +5 -1
  3. package/dist/bot/conditions.js +16 -11
  4. package/dist/bot/engine.d.ts +12 -0
  5. package/dist/bot/engine.js +55 -44
  6. package/dist/bot/strategies/claude-agent.js +2 -2
  7. package/dist/bot/strategies/funding-arb-strategy.js +16 -20
  8. package/dist/bot/strategies/funding-arb-utils.d.ts +19 -0
  9. package/dist/bot/strategies/funding-arb-utils.js +44 -0
  10. package/dist/bot/strategies/funding-auto.js +61 -37
  11. package/dist/bot/strategies/grid-strategy.d.ts +2 -0
  12. package/dist/bot/strategies/grid-strategy.js +21 -2
  13. package/dist/bot/strategies/spot-perp-arb-strategy.js +28 -20
  14. package/dist/bot/tui/BotDashboard.d.ts +13 -2
  15. package/dist/bot/tui/BotDashboard.js +8 -3
  16. package/dist/bridge-engine.d.ts +2 -0
  17. package/dist/bridge-engine.js +68 -31
  18. package/dist/cli-program.js +19 -2
  19. package/dist/commands/account.js +2 -2
  20. package/dist/commands/arb/index.d.ts +20 -0
  21. package/dist/commands/arb/index.js +121 -36
  22. package/dist/commands/arb-auto.d.ts +36 -0
  23. package/dist/commands/arb-auto.js +202 -76
  24. package/dist/commands/arb.js +51 -35
  25. package/dist/commands/bot.d.ts +6 -0
  26. package/dist/commands/bot.js +55 -45
  27. package/dist/commands/funds-onchain.d.ts +1 -1
  28. package/dist/commands/funds-onchain.js +12 -49
  29. package/dist/commands/funds.js +51 -52
  30. package/dist/commands/history.js +165 -62
  31. package/dist/commands/portfolio.d.ts +5 -0
  32. package/dist/commands/portfolio.js +58 -20
  33. package/dist/commands/rebalance.js +46 -10
  34. package/dist/commands/settings.js +2 -1
  35. package/dist/commands/trade.js +8 -4
  36. package/dist/commands/wallet.js +74 -155
  37. package/dist/cross-chain-margin.d.ts +29 -2
  38. package/dist/cross-chain-margin.js +30 -4
  39. package/dist/dashboard/server.d.ts +45 -8
  40. package/dist/dashboard/server.js +113 -42
  41. package/dist/dashboard/ui.js +46 -27
  42. package/dist/dashboard/ws-feeds.d.ts +25 -3
  43. package/dist/dashboard/ws-feeds.js +219 -82
  44. package/dist/dex-asset-map.d.ts +23 -4
  45. package/dist/dex-asset-map.js +39 -13
  46. package/dist/exchanges/arcus/adapter.js +33 -6
  47. package/dist/exchanges/arcus/public.d.ts +6 -0
  48. package/dist/exchanges/arcus/public.js +12 -4
  49. package/dist/exchanges/aster.d.ts +33 -0
  50. package/dist/exchanges/aster.js +162 -55
  51. package/dist/exchanges/hyperliquid.d.ts +30 -0
  52. package/dist/exchanges/hyperliquid.js +159 -34
  53. package/dist/exchanges/lighter.d.ts +1 -1
  54. package/dist/exchanges/lighter.js +11 -9
  55. package/dist/exchanges/ondo/adapter.d.ts +4 -18
  56. package/dist/exchanges/ondo/adapter.js +5 -29
  57. package/dist/exchanges/ondo/funds.d.ts +1 -29
  58. package/dist/exchanges/ondo/funds.js +2 -94
  59. package/dist/exchanges/pacifica.d.ts +12 -0
  60. package/dist/exchanges/pacifica.js +78 -35
  61. package/dist/exchanges/qfex/adapter.js +4 -1
  62. package/dist/exchanges/venues/ondo.d.ts +0 -5
  63. package/dist/exchanges/venues/ondo.js +2 -6
  64. package/dist/exchanges/venues/qfex.d.ts +11 -0
  65. package/dist/exchanges/venues/qfex.js +10 -2
  66. package/dist/mcp-server.js +2 -2
  67. package/dist/onchain-balances.d.ts +40 -0
  68. package/dist/onchain-balances.js +173 -0
  69. package/dist/rebalance.d.ts +20 -1
  70. package/dist/rebalance.js +31 -5
  71. package/dist/strategies/funding-arb.js +38 -7
  72. package/package.json +1 -1
  73. package/skills/perp-cli/SKILL.md +20 -3
  74. package/skills/perp-cli/references/agent-operations.md +2 -2
  75. 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 fills = await this._infoPost({ type: "userFills", user: this._address });
1780
- return (fills ?? []).slice(0, limit).map((f) => ({
1781
- orderId: String(f.oid ?? ""),
1782
- symbol: this._displaySymbol(String(f.coin ?? "")),
1783
- side: String(f.side) === "B" ? "buy" : String(f.side) === "A" ? "sell" : String(f.side).toLowerCase(),
1784
- price: String(f.px ?? "0"),
1785
- size: String(f.sz ?? ""),
1786
- filled: String(f.sz ?? ""),
1787
- status: "filled",
1788
- type: String(f.dir ?? ""),
1789
- triggerPrice: null, // fills carry no trigger price
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
- return (fills ?? []).slice(0, limit).map((f) => ({
1796
- time: Number(f.time ?? 0),
1797
- symbol: this._displaySymbol(String(f.coin ?? "")),
1798
- side: String(f.side) === "B" ? "buy" : String(f.side) === "A" ? "sell" : String(f.side).toLowerCase(),
1799
- price: String(f.px ?? "0"),
1800
- size: String(f.sz ?? ""),
1801
- fee: String(f.fee ?? "0"),
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
- // Direct API call — SDK's getUserFunding silently truncates results
1807
- const apiUrl = this._testnet ? "https://api.hyperliquid-testnet.xyz/info" : "https://api.hyperliquid.xyz/info";
1808
- const res = await fetch(apiUrl, {
1809
- method: "POST",
1810
- headers: { "Content-Type": "application/json" },
1811
- body: JSON.stringify({ type: "userFunding", user: this._address, startTime: Date.now() - 7 * 24 * 60 * 60 * 1000 }),
1812
- });
1813
- const data = await res.json();
1814
- return (data ?? []).slice(0, limit).map((h) => ({
1815
- time: h.time,
1816
- symbol: h.delta.coin + "-PERP",
1817
- payment: h.delta.usdc,
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} open order ${lighterOrderIdOf(o) || "(no id)"}: row has no ${key} (got ${JSON.stringify(v ?? null)})`, {
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: String(o.type ?? "limit"),
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: String(o.price ?? "0"),
2105
- size: String(o.initial_base_amount ?? o.base_amount ?? ""),
2106
- filled: String(o.filled_base_amount ?? "0"),
2107
- status: String(o.status ?? "done"),
2108
- type: String(o.type ?? "limit"),
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
- * Withdraw USDC from the perps (margin) wallet to an Ethereum address.
156
- * Requires an API key with the `transfer` scope. The destination may need to
157
- * be on the account's address book (venue error bad_withdrawal_address).
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(amount: string, destination: string, opts?: {
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
- * Withdraw USDC from the perps (margin) wallet to an Ethereum address.
810
- * Requires an API key with the `transfer` scope. The destination may need to
811
- * be on the account's address book (venue error bad_withdrawal_address).
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(amount, destination, opts) {
814
- if (opts?.assetId !== undefined || opts?.routeType !== undefined) {
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) + withdrawal address book.
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) + withdrawal address book.
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, verifyMessage } from "ethers";
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
  }