perp-cli 0.15.0 → 0.16.0

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 (154) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +129 -535
  4. package/README.zh.md +243 -0
  5. package/dist/api/public/lighter.d.ts +5 -0
  6. package/dist/api/public/lighter.js +5 -0
  7. package/dist/api/public/pacifica.js +2 -2
  8. package/dist/arb/sizing.js +4 -2
  9. package/dist/arb/state.d.ts +6 -0
  10. package/dist/balance-by-address.d.ts +39 -0
  11. package/dist/balance-by-address.js +44 -0
  12. package/dist/bot/quote-rules.js +1 -1
  13. package/dist/bot/strategies/funding-arb-strategy.js +10 -3
  14. package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
  15. package/dist/bot/strategies/funding-arb-utils.js +48 -15
  16. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  17. package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
  18. package/dist/bot/strategies/spot-perp-executor.js +2 -2
  19. package/dist/cli-program.js +1 -1
  20. package/dist/commands/account.js +168 -24
  21. package/dist/commands/arb/index.js +19 -77
  22. package/dist/commands/arb-auto.d.ts +24 -0
  23. package/dist/commands/arb-auto.js +155 -81
  24. package/dist/commands/arb.d.ts +8 -0
  25. package/dist/commands/arb.js +49 -23
  26. package/dist/commands/backtest.js +23 -21
  27. package/dist/commands/market.d.ts +48 -0
  28. package/dist/commands/market.js +241 -78
  29. package/dist/commands/options.d.ts +5 -0
  30. package/dist/commands/options.js +20 -3
  31. package/dist/commands/pacifica-earn.js +8 -21
  32. package/dist/commands/portfolio.d.ts +16 -0
  33. package/dist/commands/portfolio.js +51 -8
  34. package/dist/commands/trade.js +4 -4
  35. package/dist/cross-chain-margin.d.ts +3 -1
  36. package/dist/cross-chain-margin.js +4 -2
  37. package/dist/dashboard/server.js +2 -2
  38. package/dist/dashboard/ui.js +9 -6
  39. package/dist/errors.d.ts +5 -0
  40. package/dist/errors.js +10 -0
  41. package/dist/exchanges/address.d.ts +8 -0
  42. package/dist/exchanges/address.js +47 -0
  43. package/dist/exchanges/arcus/adapter.d.ts +10 -6
  44. package/dist/exchanges/arcus/adapter.js +33 -6
  45. package/dist/exchanges/arcus/public.js +3 -2
  46. package/dist/exchanges/aster.d.ts +27 -7
  47. package/dist/exchanges/aster.js +124 -42
  48. package/dist/exchanges/canonical-symbols.d.ts +7 -0
  49. package/dist/exchanges/canonical-symbols.js +20 -0
  50. package/dist/exchanges/capabilities.d.ts +2 -0
  51. package/dist/exchanges/capabilities.js +2 -1
  52. package/dist/exchanges/derive/options.d.ts +5 -0
  53. package/dist/exchanges/derive/options.js +20 -1
  54. package/dist/exchanges/extended/adapter.d.ts +16 -2
  55. package/dist/exchanges/extended/adapter.js +96 -34
  56. package/dist/exchanges/extended/public.d.ts +3 -0
  57. package/dist/exchanges/extended/public.js +9 -2
  58. package/dist/exchanges/grvt/adapter.d.ts +16 -2
  59. package/dist/exchanges/grvt/adapter.js +98 -21
  60. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  61. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  62. package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
  63. package/dist/exchanges/hyperliquid-spot.js +20 -12
  64. package/dist/exchanges/hyperliquid.d.ts +180 -13
  65. package/dist/exchanges/hyperliquid.js +487 -185
  66. package/dist/exchanges/interface.d.ts +64 -8
  67. package/dist/exchanges/interface.js +31 -0
  68. package/dist/exchanges/lighter-public.d.ts +66 -11
  69. package/dist/exchanges/lighter-public.js +249 -81
  70. package/dist/exchanges/lighter-spot.d.ts +7 -1
  71. package/dist/exchanges/lighter-spot.js +99 -90
  72. package/dist/exchanges/lighter.d.ts +70 -11
  73. package/dist/exchanges/lighter.js +285 -92
  74. package/dist/exchanges/ondo/adapter.d.ts +34 -8
  75. package/dist/exchanges/ondo/adapter.js +73 -16
  76. package/dist/exchanges/ondo/public.d.ts +27 -2
  77. package/dist/exchanges/ondo/public.js +63 -31
  78. package/dist/exchanges/outcome-interface.d.ts +4 -0
  79. package/dist/exchanges/pacifica-spot.d.ts +6 -0
  80. package/dist/exchanges/pacifica-spot.js +23 -15
  81. package/dist/exchanges/pacifica-vaults.d.ts +9 -0
  82. package/dist/exchanges/pacifica-vaults.js +14 -1
  83. package/dist/exchanges/pacifica.d.ts +77 -6
  84. package/dist/exchanges/pacifica.js +321 -130
  85. package/dist/exchanges/public-balances.d.ts +25 -0
  86. package/dist/exchanges/public-balances.js +158 -0
  87. package/dist/exchanges/public-positions.d.ts +43 -0
  88. package/dist/exchanges/public-positions.js +154 -0
  89. package/dist/exchanges/qfex/adapter.d.ts +52 -9
  90. package/dist/exchanges/qfex/adapter.js +74 -9
  91. package/dist/exchanges/qfex/public.d.ts +62 -9
  92. package/dist/exchanges/qfex/public.js +128 -22
  93. package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
  94. package/dist/exchanges/risex/adapter.d.ts +19 -7
  95. package/dist/exchanges/risex/adapter.js +58 -12
  96. package/dist/exchanges/spot-factory.js +1 -1
  97. package/dist/exchanges/txflow/adapter.d.ts +14 -7
  98. package/dist/exchanges/txflow/adapter.js +51 -19
  99. package/dist/exchanges/txflow/client.js +3 -1
  100. package/dist/exchanges/txflow/public.d.ts +12 -1
  101. package/dist/exchanges/txflow/public.js +82 -11
  102. package/dist/exchanges/unknown-symbol.d.ts +6 -1
  103. package/dist/exchanges/unknown-symbol.js +7 -2
  104. package/dist/exchanges/variational/adapter.d.ts +2 -6
  105. package/dist/exchanges/venue-types.d.ts +46 -3
  106. package/dist/exchanges/venue-types.js +80 -9
  107. package/dist/exchanges/venue.d.ts +99 -3
  108. package/dist/exchanges/venue.js +72 -0
  109. package/dist/exchanges/venues/hyperliquid.js +9 -0
  110. package/dist/exchanges/venues/lighter-rh.js +13 -3
  111. package/dist/exchanges/venues/lighter.js +13 -3
  112. package/dist/exchanges/venues/pacifica.js +9 -0
  113. package/dist/exchanges/venues/risex.js +15 -0
  114. package/dist/funding/legacy-public.d.ts +5 -3
  115. package/dist/funding/legacy-public.js +33 -37
  116. package/dist/funding/rates.d.ts +54 -5
  117. package/dist/funding/rates.js +49 -13
  118. package/dist/liquidity.d.ts +3 -1
  119. package/dist/liquidity.js +8 -4
  120. package/dist/mcp-pacifica-earn.js +11 -5
  121. package/dist/mcp-server.js +104 -15
  122. package/dist/pacifica/client.d.ts +2 -1
  123. package/dist/pacifica/client.js +11 -0
  124. package/dist/positions-by-address.d.ts +34 -0
  125. package/dist/positions-by-address.js +46 -0
  126. package/dist/strategies/funding-arb.js +35 -62
  127. package/dist/utils.d.ts +11 -1
  128. package/dist/venue-health.js +1 -1
  129. package/dist/ws/arcus-market.d.ts +9 -0
  130. package/dist/ws/arcus-market.js +9 -0
  131. package/dist/ws/aster-market.d.ts +33 -0
  132. package/dist/ws/aster-market.js +30 -0
  133. package/dist/ws/index.d.ts +30 -9
  134. package/dist/ws/index.js +180 -15
  135. package/dist/ws/lighter-market.d.ts +14 -0
  136. package/dist/ws/lighter-market.js +13 -0
  137. package/dist/ws/node.d.ts +30 -9
  138. package/dist/ws/node.js +204 -16
  139. package/dist/ws/unclassified.d.ts +16 -0
  140. package/dist/ws/unclassified.js +43 -0
  141. package/package.json +15 -2
  142. package/skills/perp-cli/SKILL.md +97 -33
  143. package/skills/perp-cli/package.json +15 -2
  144. package/skills/perp-cli/references/agent-operations.md +15 -12
  145. package/skills/perp-cli/references/commands.md +67 -12
  146. package/skills/perp-cli/references/strategies.md +38 -3
  147. package/skills/perp-cli/references/ws-library.md +55 -24
  148. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  149. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  150. package/skills/perp-cli/scripts/preflight.sh +27 -19
  151. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  152. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  153. package/dist/cli-spec.d.ts +0 -46
  154. package/dist/cli-spec.js +0 -81
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { type HistoricalAverages } from "./history.js";
14
14
  import { type VenueDefinition } from "../exchanges/venue.js";
15
+ import type { FundingRateKind } from "../exchanges/interface.js";
15
16
  import { type FundingRateBasis, type Network, type VenueId } from "../exchanges/venue-types.js";
16
17
  export interface ExchangeFundingRate {
17
18
  exchange: VenueId;
@@ -30,27 +31,61 @@ export interface ExchangeFundingRate {
30
31
  fundingVerified: boolean;
31
32
  /** How the row was read: "unofficial" = an undocumented venue API (VenueDefinition.apiStatus) — may change without notice. */
32
33
  apiStatus: "official" | "unofficial";
34
+ /**
35
+ * Which rate `fundingRate` is (the venue market row's rate — arb compares those, never a
36
+ * predicted next-period rate unless the venue only publishes that): "lastSettled" | "accruing" |
37
+ * "predicted", null = not verified (fundingRateKindOf).
38
+ */
39
+ rateKind: FundingRateKind | null;
33
40
  }
34
41
  export interface SymbolFundingComparison {
35
42
  /** Canonical symbol shared by every row. */
36
43
  symbol: string;
37
44
  rates: ExchangeFundingRate[];
38
- maxSpreadAnnual: number;
39
- longExchange: string;
40
- shortExchange: string;
45
+ /**
46
+ * Ranking is over VERIFIED rows only (VenueDefinition.fundingVerified): an unverified row
47
+ * (`--include-unverified`) is shown in `rates` but is never the long or short leg nor part of
48
+ * the spread. Fewer than two verified rows → no ranked pair: longExchange / shortExchange /
49
+ * maxSpreadAnnual / estHourlyIncomeUsd are null (a display-only row).
50
+ */
51
+ maxSpreadAnnual: number | null;
52
+ longExchange: string | null;
53
+ shortExchange: string | null;
41
54
  bestMarkPrice: number;
42
- estHourlyIncomeUsd: number;
55
+ estHourlyIncomeUsd: number | null;
43
56
  /**
44
57
  * false = a leg of the long/short pair sits on a venue without the trading role (RISEx while its
45
58
  * trading is gated in this release): the spread is data only — `arb exec` refuses it.
46
59
  */
47
60
  executable: boolean;
61
+ /**
62
+ * true = the row's rates are of different kinds (e.g. Lighter lastSettled vs Hyperliquid
63
+ * accruing) — the perp-ws comparator's `mixedKinds`: the spread compares unlike quantities.
64
+ */
65
+ mixedKinds: boolean;
48
66
  /** Venues of the long/short pair that perp-cli cannot trade (read-only); [] when executable. */
49
67
  readOnlyLegs: string[];
68
+ /**
69
+ * Venues of the long/short pair whose funding is unverified (VenueDefinition.fundingVerified
70
+ * false, e.g. QFEX — shown only with --include-unverified, display only); such a pair is never
71
+ * executable. [] when both legs are verified.
72
+ */
73
+ unverifiedLegs: string[];
50
74
  }
75
+ /** A comparison with a ranked pair (≥ 2 verified rows): long / short / spread / income are set. */
76
+ export type RankedFundingComparison = SymbolFundingComparison & {
77
+ maxSpreadAnnual: number;
78
+ longExchange: string;
79
+ shortExchange: string;
80
+ estHourlyIncomeUsd: number;
81
+ };
82
+ export declare function isRankedComparison(c: SymbolFundingComparison): c is RankedFundingComparison;
51
83
  export interface FundingRateSnapshot {
52
84
  timestamp: string;
53
85
  symbols: SymbolFundingComparison[];
86
+ /** The minSpread applied (annualized %) and how many ranked pairs it hid (spread below it). */
87
+ minSpread: number;
88
+ belowMinSpread: number;
54
89
  exchangeStatus: Record<string, "ok" | "error">;
55
90
  /** Error message per venue whose status is "error". */
56
91
  exchangeErrors: Record<string, string>;
@@ -68,6 +103,16 @@ export declare function fetchVenuesRates(venues: VenueDefinition[], net: Network
68
103
  exchangeStatus: Record<string, "ok" | "error">;
69
104
  exchangeErrors: Record<string, string>;
70
105
  }>;
106
+ /**
107
+ * Same canonical ticker on two venues is not proof of the same asset (e.g. two
108
+ * unrelated tokens both listed as "PRL"). Within a canonical group, drop any
109
+ * venue whose mark price deviates more than MAX_PRICE_DEVIATION_PCT from the
110
+ * group median, with a one-time stderr warning per (symbol, venue).
111
+ */
112
+ export declare function dropPriceOutliers<T extends {
113
+ exchange: VenueId;
114
+ markPrice: number;
115
+ }>(canonical: string, list: T[], tag?: string): T[];
71
116
  export interface VenuePrice {
72
117
  exchange: VenueId;
73
118
  /** Venue display symbol. */
@@ -94,10 +139,14 @@ export declare function fetchCanonicalPriceGroups(venues: VenueDefinition[], opt
94
139
  network?: Network;
95
140
  tag?: string;
96
141
  }): Promise<PriceGroupSnapshot>;
97
- /** Whether perp-cli can trade both legs of a long/short pair (the trading role), and which it cannot. */
142
+ /**
143
+ * Whether a long/short pair can be traded as a funding arb: both legs need the trading role AND
144
+ * verified funding (an unverified-funding venue is display only — never ranked for trading).
145
+ */
98
146
  export declare function pairExecutability(longExchange: string, shortExchange: string): {
99
147
  executable: boolean;
100
148
  readOnlyLegs: string[];
149
+ unverifiedLegs: string[];
101
150
  };
102
151
  /**
103
152
  * Fetch funding rates from every market-data venue in parallel, normalize, and compare.
@@ -14,10 +14,15 @@ import { annualizeHourlyRate, estimateHourlyFundingFromHourly } from "./normaliz
14
14
  import { getHistoricalAverages } from "./history.js";
15
15
  import { SPOT_PERP_TOKEN_MAP, MAX_PRICE_DEVIATION_PCT } from "../exchanges/index.js";
16
16
  import { getVenue, listVenuesWithRole, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
17
- import { rateToHourly } from "../exchanges/venue-types.js";
17
+ import { fundingRateKindOf, rateToHourly } from "../exchanges/venue-types.js";
18
18
  import { ensureVenuesRegistered } from "../exchanges/venues/index.js";
19
19
  import { sharedApiNetwork } from "../api/public/urls.js";
20
20
  import { PerpError } from "../errors.js";
21
+ import { canonicalQuerySymbol } from "../exchanges/canonical-symbols.js";
22
+ export function isRankedComparison(c) {
23
+ // estHourlyIncomeUsd is set together with the pair (null exactly when the pair is).
24
+ return c.longExchange !== null && c.shortExchange !== null && c.maxSpreadAnnual !== null;
25
+ }
21
26
  // ── Default top symbols to track ──
22
27
  export const TOP_SYMBOLS = [
23
28
  "BTC", "ETH", "SOL", "DOGE", "SUI", "AVAX", "LINK", "ARB",
@@ -55,13 +60,17 @@ async function fetchVenueRates(def, net) {
55
60
  for (const r of rows) {
56
61
  if (r.canonical === null)
57
62
  continue; // not matchable across venues
63
+ // Outside its funding hours (QFEX market_hours) a market has no rate: never ranked, snapshotted
64
+ // (saveFundingSnapshot) or averaged as a 0.
65
+ if (r.rate === null || r.marketClosed === true)
66
+ continue;
58
67
  const markRaw = r.markPrice !== null ? r.markPrice : markBySymbol?.get(r.venueSymbol);
59
68
  const markPrice = markRaw === undefined ? NaN : Number(markRaw);
60
69
  if (!Number.isFinite(markPrice) || markPrice <= 0) {
61
70
  unpriced.push(r.symbol);
62
71
  continue;
63
72
  }
64
- out.push(toExchangeRate(r, r.canonical, markPrice, def.fundingVerified, def.apiStatus.kind));
73
+ out.push(toExchangeRate({ ...r, rate: r.rate }, r.canonical, markPrice, def.fundingVerified, def.apiStatus.kind));
65
74
  }
66
75
  if (unpriced.length > 0) {
67
76
  console.warn(`[${def.id}] excluded ${unpriced.length} funding row(s) without a mark price: ${unpriced.join(", ")}`);
@@ -87,6 +96,7 @@ function toExchangeRate(r, canonical, markPrice, fundingVerified, apiStatus) {
87
96
  historicalAvg: null,
88
97
  fundingVerified,
89
98
  apiStatus,
99
+ rateKind: fundingRateKindOf(r.venue, r.rate),
90
100
  };
91
101
  }
92
102
  /**
@@ -122,7 +132,7 @@ const _warnedPriceMismatch = new Set();
122
132
  * venue whose mark price deviates more than MAX_PRICE_DEVIATION_PCT from the
123
133
  * group median, with a one-time stderr warning per (symbol, venue).
124
134
  */
125
- function dropPriceOutliers(canonical, list, tag = "funding") {
135
+ export function dropPriceOutliers(canonical, list, tag = "funding") {
126
136
  if (list.length < 2)
127
137
  return list;
128
138
  const sorted = list.map(r => r.markPrice).sort((x, y) => x - y);
@@ -173,7 +183,7 @@ function groupByCanonical(rates, symbols, tag = "funding") {
173
183
  export async function fetchCanonicalPriceGroups(venues, opts) {
174
184
  ensureVenuesRegistered();
175
185
  const net = opts?.network ?? sharedApiNetwork();
176
- const settled = await Promise.allSettled(venues.map(v => v.publicData.fetchMarkets(net)));
186
+ const settled = await Promise.allSettled(venues.map(v => v.publicData.fetchMarkets(net, { funding: false })));
177
187
  const exchangeStatus = {};
178
188
  const exchangeErrors = {};
179
189
  const prices = [];
@@ -196,10 +206,15 @@ export async function fetchCanonicalPriceGroups(venues, opts) {
196
206
  });
197
207
  return { groups: groupByCanonical(prices, opts?.symbols, opts?.tag ?? "prices"), exchangeStatus, exchangeErrors };
198
208
  }
199
- /** Whether perp-cli can trade both legs of a long/short pair (the trading role), and which it cannot. */
209
+ /**
210
+ * Whether a long/short pair can be traded as a funding arb: both legs need the trading role AND
211
+ * verified funding (an unverified-funding venue is display only — never ranked for trading).
212
+ */
200
213
  export function pairExecutability(longExchange, shortExchange) {
201
- const readOnlyLegs = [...new Set([longExchange, shortExchange])].filter(v => !getVenue(v).roles.includes("trading"));
202
- return { executable: readOnlyLegs.length === 0, readOnlyLegs };
214
+ const legs = [...new Set([longExchange, shortExchange])];
215
+ const readOnlyLegs = legs.filter(v => !getVenue(v).roles.includes("trading"));
216
+ const unverifiedLegs = legs.filter(v => !getVenue(v).fundingVerified);
217
+ return { executable: readOnlyLegs.length === 0 && unverifiedLegs.length === 0, readOnlyLegs, unverifiedLegs };
203
218
  }
204
219
  function bestMarkPrice(rates) {
205
220
  const rank = (id) => MARK_PRICE_PREFERENCE.indexOf(id);
@@ -226,14 +241,28 @@ export async function fetchAllFundingRates(opts) {
226
241
  }
227
242
  const comparisons = [];
228
243
  const minSpread = opts?.minSpread ?? 0;
244
+ let belowMinSpread = 0;
229
245
  for (const [symbol, rates] of rateMap) {
230
246
  // Need at least 2 exchanges to compare
231
247
  if (rates.length < 2)
232
248
  continue;
233
249
  // Sort by hourly rate (ascending)
234
250
  rates.sort((a, b) => a.hourlyRate - b.hourlyRate);
235
- const lowest = rates[0];
236
- const highest = rates[rates.length - 1];
251
+ // The long / short legs and the spread come from verified rows only: an unverified rate is
252
+ // display data (--include-unverified), never ranked. < 2 verified rows → a display-only row.
253
+ const ranked = rates.filter(r => r.fundingVerified);
254
+ if (ranked.length < 2) {
255
+ const unverifiedLegs = [...new Set(rates.filter(r => !r.fundingVerified).map(r => r.exchange))];
256
+ comparisons.push({
257
+ symbol, rates, maxSpreadAnnual: null, longExchange: null, shortExchange: null,
258
+ bestMarkPrice: bestMarkPrice(rates), estHourlyIncomeUsd: null,
259
+ executable: false, readOnlyLegs: [], unverifiedLegs,
260
+ mixedKinds: new Set(rates.map(r => r.rateKind)).size > 1,
261
+ });
262
+ continue;
263
+ }
264
+ const lowest = ranked[0];
265
+ const highest = ranked[ranked.length - 1];
237
266
  // The median filter alone lets a 2-venue group through at up to 2 × MAX_PRICE_DEVIATION_PCT
238
267
  // apart (each side deviates half the gap): the CHOSEN pair must itself agree on price.
239
268
  const pairGapPct = Math.abs(lowest.markPrice - highest.markPrice) / ((lowest.markPrice + highest.markPrice) / 2) * 100;
@@ -246,8 +275,10 @@ export async function fetchAllFundingRates(opts) {
246
275
  continue;
247
276
  }
248
277
  const maxSpreadAnnual = annualizeHourlyRate(highest.hourlyRate - lowest.hourlyRate);
249
- if (maxSpreadAnnual < minSpread)
278
+ if (maxSpreadAnnual < minSpread) {
279
+ belowMinSpread++;
250
280
  continue;
281
+ }
251
282
  // Estimate hourly income for $1000 notional arb
252
283
  const notional = 1000;
253
284
  const longIncome = estimateHourlyFundingFromHourly(lowest.hourlyRate, notional, "long");
@@ -262,13 +293,16 @@ export async function fetchAllFundingRates(opts) {
262
293
  bestMarkPrice: bestMarkPrice(rates),
263
294
  estHourlyIncomeUsd,
264
295
  ...pairExecutability(lowest.exchange, highest.exchange),
296
+ mixedKinds: new Set(rates.map(r => r.rateKind)).size > 1,
265
297
  });
266
298
  }
267
- // Sort by spread descending
268
- comparisons.sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual);
299
+ // Sort by spread descending; display-only rows (no ranked pair) last
300
+ comparisons.sort((a, b) => (b.maxSpreadAnnual ?? -Infinity) - (a.maxSpreadAnnual ?? -Infinity));
269
301
  return {
270
302
  timestamp: new Date().toISOString(),
271
303
  symbols: comparisons,
304
+ minSpread,
305
+ belowMinSpread,
272
306
  exchangeStatus,
273
307
  exchangeErrors,
274
308
  };
@@ -277,7 +311,9 @@ export async function fetchAllFundingRates(opts) {
277
311
  * Fetch rates for a single (canonical) symbol across all exchanges.
278
312
  */
279
313
  export async function fetchSymbolFundingRates(symbol, includeUnverified = false) {
280
- const snapshot = await fetchAllFundingRates({ symbols: [symbol.toUpperCase()], includeUnverified });
314
+ // The comparison groups by canonical key: a venue ticker / denomination alias (kPEPE) is
315
+ // looked up under its canonical symbol (1000PEPE), never as-is.
316
+ const snapshot = await fetchAllFundingRates({ symbols: [canonicalQuerySymbol(symbol)], includeUnverified });
281
317
  return snapshot.symbols[0] ?? null;
282
318
  }
283
319
  /**
@@ -26,7 +26,9 @@ export declare function computeExecutableSize(levels: [string, string][], reques
26
26
  * Check liquidity on both sides for an arb entry.
27
27
  * Returns adjusted size or 0 if not viable.
28
28
  */
29
- export declare function checkArbLiquidity(longAdapter: ExchangeAdapter, shortAdapter: ExchangeAdapter, symbol: string, sizeUsd: number, maxSlippagePct?: number, log?: (msg: string) => void): Promise<{
29
+ export declare function checkArbLiquidity(longAdapter: ExchangeAdapter, shortAdapter: ExchangeAdapter, symbol: string, sizeUsd: number, maxSlippagePct?: number, log?: (msg: string) => void,
30
+ /** The short venue's own symbol when it differs (kPEPE ↔ 1000PEPE); default `symbol`. */
31
+ shortSymbol?: string): Promise<{
30
32
  viable: boolean;
31
33
  adjustedSizeUsd: number;
32
34
  longSlippage: number;
package/dist/liquidity.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { classifyError } from "./errors.js";
1
2
  /**
2
3
  * Walk through orderbook levels and compute how much can be filled
3
4
  * within the given slippage tolerance.
@@ -63,11 +64,13 @@ export function computeExecutableSize(levels, requestedSizeUsd, maxSlippagePct =
63
64
  * Check liquidity on both sides for an arb entry.
64
65
  * Returns adjusted size or 0 if not viable.
65
66
  */
66
- export async function checkArbLiquidity(longAdapter, shortAdapter, symbol, sizeUsd, maxSlippagePct = 0.5, log) {
67
+ export async function checkArbLiquidity(longAdapter, shortAdapter, symbol, sizeUsd, maxSlippagePct = 0.5, log,
68
+ /** The short venue's own symbol when it differs (kPEPE ↔ 1000PEPE); default `symbol`. */
69
+ shortSymbol = symbol) {
67
70
  try {
68
71
  const [longOB, shortOB] = await Promise.all([
69
72
  longAdapter.getOrderbook(symbol),
70
- shortAdapter.getOrderbook(symbol),
73
+ shortAdapter.getOrderbook(shortSymbol),
71
74
  ]);
72
75
  // For long entry we consume asks, for short entry we consume bids
73
76
  const longCheck = computeExecutableSize(longOB.asks, sizeUsd, maxSlippagePct);
@@ -145,8 +148,9 @@ export async function checkSpotPerpLiquidity(spotAdapter, perpAdapter, spotSymbo
145
148
  perpSlippage: perpCheck.slippagePct,
146
149
  };
147
150
  }
148
- catch {
149
- log?.(`[LIQ] spot-perp ${spotSymbol}: orderbook fetch failed`);
151
+ catch (e) {
152
+ const c = classifyError(e);
153
+ log?.(`[LIQ] spot-perp ${spotSymbol}: orderbook fetch failed (${c.code}: ${c.message})`);
150
154
  return { viable: false, adjustedSizeUsd: 0, spotSlippage: 0, perpSlippage: 0 };
151
155
  }
152
156
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
- import { PacificaClient } from "./pacifica/client.js";
2
+ import { PacificaClient, requireSolanaAddress } from "./pacifica/client.js";
3
3
  import { fetchPrintGames, fetchPrintHistory, fetchPrintMarket, fetchPrintPositions, findPrintGame, quotePrintOrder } from "./exchanges/pacifica-print.js";
4
- import { DEPOSIT_WHITELIST_API_STATUS, fetchDepositWhitelist, fetchVaults, findVault, heldShares, withdrawWindowAt } from "./exchanges/pacifica-vaults.js";
4
+ import { DEPOSIT_WHITELIST_API_STATUS, readDepositWhitelist, fetchVaults, findVault, heldShares, withdrawWindowAt } from "./exchanges/pacifica-vaults.js";
5
5
  export function registerPacificaEarnMcpTools(server, network, ok, errOf) {
6
6
  const client = () => new PacificaClient({ network });
7
7
  const run = async (meta, f) => {
@@ -12,7 +12,13 @@ export function registerPacificaEarnMcpTools(server, network, ok, errOf) {
12
12
  return { content: [{ type: "text", text: errOf(e, meta) }], isError: true };
13
13
  }
14
14
  };
15
- const address = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/, "base58 Solana address");
15
+ const address = z.string().refine((v) => { try {
16
+ requireSolanaAddress(v);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ } }, "base58 Solana address (32-byte public key; not an EVM address)");
16
22
  server.tool("pacifica_print_products", "Pacifica Print products (read-only): game id (e.g. BTC_24H, BTC_7D, BTC_1M), cycle length in days, max leverage, deposit caps, allowed target distance from mark. Print = a strike order that earns a premium each cycle; while resting it fills / is liquidated only at a cycle checkpoint, after a fill it is an ordinary perp position (continuous liquidation).", {}, async () => run({ exchange: "pacifica" }, async () => fetchPrintGames(client())));
17
23
  server.tool("pacifica_print_quote", "Validate a Pacifica Print order and preview it (read-only, nothing signed): notional, size on fill, docs-formula liquidation price (an outer bound — a liquidation was observed earlier in one live case; venue trigger unpublished), max loss (deposit + premiums), lock period. The premium is not quoted (the venue's preview is a signed request). Placing the order is CLI-only: perp -e pacifica print deposit … (supports --dry-run).", {
18
24
  game: z.string().describe("Print product, e.g. BTC_24H"),
@@ -46,12 +52,12 @@ export function registerPacificaEarnMcpTools(server, network, ok, errOf) {
46
52
  if (vault === undefined)
47
53
  return vaults;
48
54
  const v = findVault(vaults, vault);
49
- const [acct, positions, whitelist] = await Promise.all([c.getAccount(v.address), c.getPositions(v.address), fetchDepositWhitelist(c, v.address).catch(() => null)]);
55
+ const [acct, positions, whitelist] = await Promise.all([c.getAccount(v.address), c.getPositions(v.address), readDepositWhitelist(c, v.address)]);
50
56
  return {
51
57
  ...v,
52
58
  ...(account !== undefined ? { holding: heldShares(v, account) } : {}),
53
59
  live: { accountEquity: acct.account_equity ?? null, availableToWithdraw: acct.available_to_withdraw ?? null, positions: positions.map((p) => ({ symbol: p.symbol, side: p.side, amount: p.amount, entryPrice: p.entry_price })) },
54
- depositWhitelist: { apiStatus: DEPOSIT_WHITELIST_API_STATUS, addresses: whitelist },
60
+ depositWhitelist: { apiStatus: DEPOSIT_WHITELIST_API_STATUS, addresses: whitelist.addresses, error: whitelist.error },
55
61
  withdrawWindow: withdrawWindowAt(v.config, Date.now()),
56
62
  };
57
63
  }));
@@ -23,8 +23,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24
24
  import { z } from "zod";
25
25
  import { hasCredentials, listConfiguredAccountVenues, resolveCredentials, NO_CLI_CREDENTIAL_OPTS } from "./config.js";
26
- import { getVenue, listVenues, listVenuesWithRole, networkConfig, READ_ONLY_CREDENTIALS, requireCapabilities, roleUnsupported } from "./exchanges/venue.js";
26
+ import { getVenue, listPublicBalanceVenues, listPublicPositionVenues, listVenues, listVenuesWithRole, networkConfig, READ_ONLY_CREDENTIALS, requireCapabilities, roleUnsupported } from "./exchanges/venue.js";
27
27
  import "./exchanges/venues/index.js";
28
+ import { venueMarketRow } from "./exchanges/venue-types.js";
29
+ import { lighterFundingStatsWarning } from "./exchanges/lighter-public.js";
28
30
  import { newVenueClientOrderId, placeAndConfirm, validateLegacyTif, validateOrderRequest } from "./exchanges/trading-v2.js";
29
31
  import { closePerpVerified, isOutcomeUnknown } from "./bot/perp-order.js";
30
32
  import { quarantineSymbol } from "./bot/quarantine.js";
@@ -34,7 +36,9 @@ import { PerpError, classifyError, extractErrorMessage, isDefiniteOrderFailure,
34
36
  import { invalidateCache } from "./cache.js";
35
37
  import { probeVenueHealth } from "./venue-health.js";
36
38
  import { getOptionsVenue, listOptionsVenueIds } from "./exchanges/options-venues.js";
37
- import { fetchAllFundingRates } from "./funding-rates.js";
39
+ import { fetchAllFundingRates, isRankedComparison } from "./funding-rates.js";
40
+ import { readPositionsByAddress } from "./positions-by-address.js";
41
+ import { readBalanceByAddress, readBalanceByAddressAll } from "./balance-by-address.js";
38
42
  import { loadArbState } from "./arb/state.js";
39
43
  import { validateTrade } from "./trade-validator.js";
40
44
  import { orderResultJson, symbolMatch } from "./utils.js";
@@ -130,15 +134,38 @@ async function tryGetAdapter(exchange) {
130
134
  async function fetchMarketsPublic(exchange) {
131
135
  return labelMarketRows(exchange, await getVenue(exchange).publicData.fetchMarkets(MCP_NETWORK));
132
136
  }
137
+ /**
138
+ * meta.warnings when a venue's rows carry fundingRate null for a known reason (Lighter: the
139
+ * settled-rate snapshot could not be read) — the reason reaches the agent, not only stderr.
140
+ */
141
+ function fundingWarningMeta(exchange, rows) {
142
+ const id = getVenue(exchange).id;
143
+ if (id !== "lighter" && id !== "lighter-rh")
144
+ return {};
145
+ // Tied to the rows served: only when they actually carry a null rate (cached rows that have
146
+ // rates never get a stale warning; null rows always say why when the reason is known).
147
+ if (!rows.some(r => r.fundingRate === null))
148
+ return {};
149
+ const w = lighterFundingStatsWarning(id, MCP_NETWORK);
150
+ return w === null ? {} : { warnings: [w] };
151
+ }
152
+ /**
153
+ * Keyless get_markets rows: the public directory's VenueMarket rows in the CLI market-row shape
154
+ * (venueMarketRow — the mapper the VenueMarket CLI adapters use), the VenueMarket fields kept
155
+ * (venueSymbol, canonical, assetClass, fundingRateBasis, tickSize, …) and the MCP labels added.
156
+ */
157
+ async function fetchMarketRowsPublic(exchange) {
158
+ const markets = await getVenue(exchange).publicData.fetchMarkets(MCP_NETWORK);
159
+ return labelMarketRows(exchange, markets.map(m => ({ ...m, ...venueMarketRow(m) })));
160
+ }
133
161
  /**
134
162
  * The one row label for market rows of any source (public data path or a credentialed adapter):
135
- * unofficial-API rows carry venueApi + fundingVerified; unverified-funding rows carry fundingVerified false.
163
+ * every row carries fundingVerified (true or false — as `perp market list`), unofficial-API rows venueApi.
136
164
  */
137
165
  function labelMarketRows(exchange, rows) {
138
166
  const def = getVenue(exchange);
139
- if (def.apiStatus.kind === "unofficial")
140
- return rows.map(r => ({ ...r, venueApi: "unofficial", fundingVerified: def.fundingVerified }));
141
- return def.fundingVerified ? rows : rows.map(r => ({ ...r, fundingVerified: false }));
167
+ const labelled = rows.map(r => ({ ...r, fundingVerified: def.fundingVerified }));
168
+ return def.apiStatus.kind === "unofficial" ? labelled.map(r => ({ ...r, venueApi: "unofficial" })) : labelled;
142
169
  }
143
170
  /**
144
171
  * meta fields every market-data tool adds for its venue: apiStatus, the note for an unofficial API,
@@ -261,10 +288,13 @@ server.tool("get_markets", `Get all available perpetual futures markets on an ex
261
288
  const adapter = await tryGetAdapter(exchange);
262
289
  if (adapter) {
263
290
  const markets = labelMarketRows(exchange, await adapter.getMarkets());
264
- return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, ...apiMeta(exchange) }) }] };
291
+ return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, ...apiMeta(exchange), ...fundingWarningMeta(exchange, markets) }) }] };
265
292
  }
266
- const markets = await fetchMarketsPublic(exchange);
267
- return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, source: "public", ...apiMeta(exchange) }) }] };
293
+ // No credentials: the venue's public directory — price-only on GRVT (no 184-request
294
+ // margin_rules fan-out per call) — in the CLI market-row shape (venueMarketRow), with the
295
+ // same market set as `perp market list` (the venue's shared eligibility / listing rule).
296
+ const markets = await fetchMarketRowsPublic(exchange);
297
+ return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, source: "public", ...apiMeta(exchange), ...fundingWarningMeta(exchange, markets) }) }] };
268
298
  }
269
299
  catch (e) {
270
300
  return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
@@ -329,6 +359,10 @@ server.tool("get_prices", "Get cross-exchange prices for symbols. Fetches mark p
329
359
  return;
330
360
  }
331
361
  for (const m of r.value) {
362
+ // An ambiguous ticker (SPX6900 vs the S&P 500) names different instruments per venue:
363
+ // never grouped by symbol across venues.
364
+ if (m.assetClass === "unclassified")
365
+ continue;
332
366
  const sym = m.symbol.toUpperCase();
333
367
  if (filter && !filter.has(sym))
334
368
  continue;
@@ -477,6 +511,49 @@ server.tool("get_positions", `Get all open positions on an exchange, including s
477
511
  return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
478
512
  }
479
513
  });
514
+ server.tool("get_positions_by_address", `Open positions of ANY account by its public address — keyless, read-only (public venue data; nothing is signed, no key is needed). Venues: ${listPublicPositionVenues().map(v => `${v.id} (${v.publicData.addressKind === "solana" ? "base58 Solana address" : "0x address"})`).join(", ")}. Hyperliquid reads the native dex and every HIP-3 dex (rows carry dex: null | "<dex>"; a dex that fails is listed in meta.errors / meta.warnings — its rows are missing, not absent); Lighter reads every account of the address (rows carry accountIndex; accountIndex narrows). Other venues: CAPABILITY_UNSUPPORTED; a wrong address format: INVALID_PARAMS. Same rows as \`perp -e <venue> account positions --address <addr>\`.`, {
515
+ exchange: z.string().describe(`Exchange: ${listPublicPositionVenues().map(v => v.id).join(", ")} (aliases accepted)`),
516
+ address: z.string().describe("Public account address (0x… for hyperliquid / lighter / lighter-rh; base58 Solana for pacifica)"),
517
+ symbol: z.string().optional().describe("Only this market (plain asset, e.g. BTC)"),
518
+ accountIndex: z.number().int().nonnegative().optional().describe("Lighter only: one account (sub-account) of the address"),
519
+ }, async ({ exchange, address, symbol, accountIndex }) => {
520
+ try {
521
+ const r = await readPositionsByAddress(exchange, MCP_NETWORK, address, { symbol, accountIndex });
522
+ return { content: [{ type: "text", text: ok(r.positions, {
523
+ exchange: r.venue, address, source: "public", count: r.positions.length,
524
+ ...(r.warnings.length > 0 ? { warnings: r.warnings, errors: r.errors } : {}),
525
+ ...(r.notes.length > 0 ? { notes: r.notes } : {}),
526
+ }) }] };
527
+ }
528
+ catch (e) {
529
+ return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
530
+ }
531
+ });
532
+ server.tool("get_balance_by_address", `Balances of ANY account by its public address — keyless, read-only (public venue data; nothing is signed, no key is needed). Venues: ${listPublicBalanceVenues().map(v => `${v.id} (${v.publicData.addressKind === "solana" ? "base58 Solana address" : "0x address"})`).join(", ")}; omit exchange to read every one of them (a venue that fails, address format included, is listed in data.errors). data: { balance (equity / available / marginUsed / unrealizedPnl + venue extras; null on Lighter), spot (token balances; null where the venue does not publish them), accounts (Lighter: one row per account of the address; accountIndex narrows), dexes + accountMode (Hyperliquid: HIP-3 dexes with a non-zero account value; a dex that fails is in meta.errors / meta.warnings — unknown, not zero) }. Other venues: CAPABILITY_UNSUPPORTED; a wrong address format: INVALID_PARAMS. Same data as \`perp [-e <venue>] account balance --address <addr>\`.`, {
533
+ exchange: z.string().optional().describe(`Exchange: ${listPublicBalanceVenues().map(v => v.id).join(", ")} (aliases accepted); omit for every one`),
534
+ address: z.string().describe("Public account address (0x… for hyperliquid / lighter / lighter-rh / risex; base58 Solana for pacifica)"),
535
+ accountIndex: z.number().int().nonnegative().optional().describe("Lighter only: one account (sub-account) of the address"),
536
+ }, async ({ exchange, address, accountIndex }) => {
537
+ try {
538
+ if (exchange === undefined) {
539
+ const all = await readBalanceByAddressAll(MCP_NETWORK, address, { accountIndex });
540
+ return { content: [{ type: "text", text: ok({
541
+ venues: all.venues,
542
+ ...(Object.keys(all.errors).length > 0 ? { errors: all.errors } : {}),
543
+ ...(Object.keys(all.notes).length > 0 ? { notes: all.notes } : {}),
544
+ }, { address, source: "public", ...(all.warnings.length > 0 ? { warnings: all.warnings } : {}) }) }] };
545
+ }
546
+ const r = await readBalanceByAddress(exchange, MCP_NETWORK, address, { accountIndex });
547
+ return { content: [{ type: "text", text: ok({ exchange: r.venue, address, ...r.data }, {
548
+ exchange: r.venue, address, source: "public",
549
+ ...(r.warnings.length > 0 ? { warnings: r.warnings, errors: r.errors } : {}),
550
+ ...(r.notes.length > 0 ? { notes: r.notes } : {}),
551
+ }) }] };
552
+ }
553
+ catch (e) {
554
+ return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
555
+ }
556
+ });
480
557
  server.tool("get_open_orders", `Get all open/pending orders on an exchange. ${HL_SCOPE_NOTE}`, { exchange: z.string().describe(`Exchange name: ${listVenues().map(v => v.id).join(", ")} (aliases accepted)`) }, async ({ exchange }) => {
481
558
  try {
482
559
  const adapter = await getOrCreateAdapter(exchange);
@@ -510,6 +587,9 @@ server.tool("portfolio", "Cross-exchange portfolio summary: balances, positions,
510
587
  };
511
588
  }
512
589
  }));
590
+ // Totals are null (unknown) when no account balance was read — no configured credentials, or
591
+ // every venue failed — never 0 (QA 0.15.0: totalEquity 0 without credentials).
592
+ const read = snapshots.filter(s => s.balance !== null);
513
593
  let totalEquity = 0;
514
594
  let totalAvailable = 0;
515
595
  let totalMarginUsed = 0;
@@ -545,16 +625,21 @@ server.tool("portfolio", "Cross-exchange portfolio summary: balances, positions,
545
625
  pct: totalEquity > 0 ? (Number(s.balance.equity) / totalEquity) * 100 : 0,
546
626
  }))
547
627
  .sort((a, b) => b.pct - a.pct);
628
+ const known = read.length > 0;
548
629
  const summary = {
549
- totalEquity,
550
- totalAvailable,
551
- totalMarginUsed,
552
- totalUnrealizedPnl,
630
+ /** "noAccount" = no venue has credentials; "unread" = credentials exist but no balance was read. */
631
+ accountState: EXCHANGES.length === 0 ? "noAccount" : known ? "ok" : "unread",
632
+ /** Venues whose balances the totals cover. */
633
+ totalsCover: read.map(s => s.exchange),
634
+ totalEquity: known ? totalEquity : null,
635
+ totalAvailable: known ? totalAvailable : null,
636
+ totalMarginUsed: known ? totalMarginUsed : null,
637
+ totalUnrealizedPnl: known ? totalUnrealizedPnl : null,
553
638
  totalPositions,
554
639
  totalOpenOrders,
555
640
  exchanges: snapshots,
556
641
  positions: allPositions,
557
- riskMetrics: { marginUtilization, largestPosition, exchangeConcentration },
642
+ riskMetrics: { marginUtilization: known ? marginUtilization : null, largestPosition, exchangeConcentration },
558
643
  };
559
644
  return { content: [{ type: "text", text: ok(summary) }] };
560
645
  });
@@ -1198,13 +1283,14 @@ server.tool("arb_scan", "Scan for funding rate arbitrage opportunities across ex
1198
1283
  symbols,
1199
1284
  minSpread: minSpread ?? 5,
1200
1285
  });
1201
- const opportunities = snapshot.symbols.map(s => ({
1286
+ const opportunities = snapshot.symbols.filter(isRankedComparison).map(s => ({
1202
1287
  symbol: s.symbol,
1203
1288
  maxSpreadAnnual: `${s.maxSpreadAnnual.toFixed(2)}%`,
1204
1289
  strategy: `Long ${s.longExchange} / Short ${s.shortExchange}`,
1205
1290
  // false = a leg is on a read-only venue (RISEx in this release): data only, not tradable here.
1206
1291
  executable: s.executable,
1207
1292
  readOnlyLegs: s.readOnlyLegs,
1293
+ unverifiedLegs: s.unverifiedLegs,
1208
1294
  estHourlyIncomePerK: `$${s.estHourlyIncomeUsd.toFixed(4)}`,
1209
1295
  rates: s.rates.map(r => ({
1210
1296
  exchange: r.exchange,
@@ -1236,6 +1322,9 @@ server.tool("health_check", "Ping every registered venue on the server's network
1236
1322
  result.covered = health.probes.map(p => p.venue);
1237
1323
  // A venue without an environment on the server's network (TxFlow has no testnet) is not probed.
1238
1324
  result.skipped = health.skipped;
1325
+ // Same two verdicts as `perp health --json`: healthy = every OFFICIAL-API venue is up (an
1326
+ // unofficial API being down does not make perp-cli unhealthy); allHealthy includes them.
1327
+ result.healthy = health.probes.filter(p => p.apiStatus === "official").every(p => p.ok);
1239
1328
  result.allHealthy = health.allHealthy;
1240
1329
  return { content: [{ type: "text", text: ok(result) }] };
1241
1330
  }
@@ -1,4 +1,5 @@
1
1
  import { type Network } from "./constants.js";
2
+ import { requireSolanaAddress } from "../exchanges/address.js";
2
3
  import type { MarketInfo, PriceInfo, Orderbook, Trade, Kline, KlineInterval, AggLevel, AccountInfo, AccountSettings, Position, OrderInfo, MarketOrderParams, LimitOrderParams, StopOrderParams, EditOrderParams, CancelOrderParams, CancelAllOrdersParams, TWAPParams, CancelTWAPParams, TPSLParams, UpdateLeverageParams, WithdrawParams, TransferFundsParams, BatchAction, BatchActionType, CreateLakeParams, LakeDepositParams, LakeWithdrawParams } from "./types/index.js";
3
4
  type SignMessageFn = (message: Uint8Array) => Promise<Uint8Array>;
4
5
  /** Per-request timeout of a POST (fetch + body read). */
@@ -16,6 +17,7 @@ export interface PacificaClientConfig {
16
17
  baseUrl?: string;
17
18
  apiKey?: string;
18
19
  }
20
+ export { requireSolanaAddress };
19
21
  export declare class PacificaClient {
20
22
  private baseUrl;
21
23
  private apiKey?;
@@ -202,4 +204,3 @@ export declare class PacificaClient {
202
204
  expiry_window: number;
203
205
  }): Promise<Record<string, unknown>>;
204
206
  }
205
- export {};
@@ -2,6 +2,7 @@ import { getNetworkConfig } from "./constants.js";
2
2
  import { buildSignedRequest, buildAgentSignedRequest, signWithWallet } from "./signing.js";
3
3
  import { PerpError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure } from "../errors.js";
4
4
  import { invalidateCache } from "../cache.js";
5
+ import { requireSolanaAddress } from "../exchanges/address.js";
5
6
  import { PACIFICA_COST_CANCEL, PACIFICA_COST_STANDARD, PACIFICA_MAX_THROTTLE_WAIT_MS, PACIFICA_READ_NEED, PACIFICA_SIGNED_MAX_WAIT_MS, pacificaRateLimiter, } from "./rate-limit.js";
6
7
  /** Per-request timeout of a POST (fetch + body read). */
7
8
  export const PACIFICA_POST_TIMEOUT_MS = 15_000;
@@ -38,6 +39,14 @@ function writeNeed(cancel, maxWaitMs) {
38
39
  export function pacificaBatchCost(types) {
39
40
  return types.reduce((a, t) => a + (t === "Cancel" || t === "CancelStopOrder" ? PACIFICA_COST_CANCEL : PACIFICA_COST_STANDARD), 0);
40
41
  }
42
+ /** Query parameters that carry a Solana address (account reads, Print orders, vaults). */
43
+ const ADDRESS_PARAMS = new Set(["account", "game_account", "lake"]);
44
+ export { requireSolanaAddress };
45
+ function checkAddressParams(params) {
46
+ for (const [k, v] of Object.entries(params ?? {}))
47
+ if (ADDRESS_PARAMS.has(k) && v !== undefined && v !== null)
48
+ requireSolanaAddress(v, k);
49
+ }
41
50
  export class PacificaClient {
42
51
  baseUrl;
43
52
  apiKey;
@@ -87,6 +96,7 @@ export class PacificaClient {
87
96
  return h;
88
97
  }
89
98
  async get(path, params) {
99
+ checkAddressParams(params);
90
100
  const url = new URL(`${this.baseUrl}${path}`);
91
101
  if (params) {
92
102
  Object.entries(params).forEach(([k, v]) => {
@@ -445,6 +455,7 @@ export class PacificaClient {
445
455
  * non-JSON / non-object body → EXCHANGE_ERROR (a read: nothing can have changed).
446
456
  */
447
457
  async getEnvelope(path, params = {}) {
458
+ checkAddressParams(params);
448
459
  const url = new URL(`${this.baseUrl}${path}`);
449
460
  for (const [k, v] of Object.entries(params))
450
461
  url.searchParams.set(k, v);
@@ -0,0 +1,34 @@
1
+ import { type Network, type PublicPosition } from "./exchanges/venue.js";
2
+ export interface AddressPositions {
3
+ venue: string;
4
+ positions: PublicPosition[];
5
+ /** Sub-reads that failed (Hyperliquid HIP-3 dex), keyed "hip3:<dex>". */
6
+ errors: Record<string, {
7
+ code: string;
8
+ message: string;
9
+ }>;
10
+ notes: string[];
11
+ /** One human line per failed sub-read: its rows are unknown, not absent. */
12
+ warnings: string[];
13
+ }
14
+ export interface AddressPositionsOptions {
15
+ accountIndex?: number;
16
+ symbol?: string;
17
+ }
18
+ /** Rows of `symbol` (plain asset; HIP-3 "xyz:GOLD" also matches "GOLD"; "BTC-PERP" matches "BTC"). */
19
+ export declare function filterPositionsBySymbol<T extends {
20
+ symbol: string;
21
+ }>(rows: T[], symbol: string | undefined): T[];
22
+ export declare function readPositionsByAddress(venue: string, net: Network, address: string, opts?: AddressPositionsOptions): Promise<AddressPositions>;
23
+ export interface AddressPositionsFanOut {
24
+ venues: Record<string, PublicPosition[]>;
25
+ /** A venue that failed (`<venue>`) or a failed sub-read of a venue (`<venue>:hip3:<dex>`). */
26
+ errors: Record<string, {
27
+ code: string;
28
+ message: string;
29
+ }>;
30
+ notes: Record<string, string[]>;
31
+ warnings: string[];
32
+ }
33
+ /** Every venue that reads positions by address; a venue that fails (address format included) is an `errors` entry. */
34
+ export declare function readPositionsByAddressAll(net: Network, address: string, opts?: AddressPositionsOptions): Promise<AddressPositionsFanOut>;