pmxt-core 2.50.16 → 2.51.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.
- package/dist/exchanges/hunch/api.d.ts +736 -0
- package/dist/exchanges/hunch/api.js +820 -0
- package/dist/exchanges/hunch/auth.d.ts +36 -0
- package/dist/exchanges/hunch/auth.js +63 -0
- package/dist/exchanges/hunch/errors.d.ts +26 -0
- package/dist/exchanges/hunch/errors.js +94 -0
- package/dist/exchanges/hunch/fetcher.d.ts +204 -0
- package/dist/exchanges/hunch/fetcher.js +130 -0
- package/dist/exchanges/hunch/index.d.ts +51 -0
- package/dist/exchanges/hunch/index.js +330 -0
- package/dist/exchanges/hunch/normalizer.d.ts +56 -0
- package/dist/exchanges/hunch/normalizer.js +180 -0
- package/dist/exchanges/hunch/price.d.ts +5 -0
- package/dist/exchanges/hunch/price.js +12 -0
- package/dist/exchanges/hunch/utils.d.ts +79 -0
- package/dist/exchanges/hunch/utils.js +182 -0
- package/dist/exchanges/hunch/websocket.d.ts +25 -0
- package/dist/exchanges/hunch/websocket.js +137 -0
- package/dist/exchanges/hyperliquid/auth.js +15 -2
- package/dist/exchanges/hyperliquid/fetcher.d.ts +26 -1
- package/dist/exchanges/hyperliquid/fetcher.js +66 -4
- package/dist/exchanges/hyperliquid/index.d.ts +4 -1
- package/dist/exchanges/hyperliquid/index.js +84 -20
- package/dist/exchanges/hyperliquid/normalizer.d.ts +4 -3
- package/dist/exchanges/hyperliquid/normalizer.js +69 -11
- package/dist/exchanges/hyperliquid/utils.d.ts +7 -1
- package/dist/exchanges/hyperliquid/utils.js +16 -4
- package/dist/exchanges/kalshi/api.d.ts +1 -1
- package/dist/exchanges/kalshi/api.js +1 -1
- package/dist/exchanges/limitless/api.d.ts +1 -1
- package/dist/exchanges/limitless/api.js +1 -1
- package/dist/exchanges/myriad/api.d.ts +1 -1
- package/dist/exchanges/myriad/api.js +1 -1
- package/dist/exchanges/opinion/api.d.ts +1 -1
- package/dist/exchanges/opinion/api.js +1 -1
- package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
- package/dist/exchanges/polymarket/api-clob.js +1 -1
- package/dist/exchanges/polymarket/api-data.d.ts +1 -1
- package/dist/exchanges/polymarket/api-data.js +1 -1
- package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
- package/dist/exchanges/polymarket/api-gamma.js +1 -1
- package/dist/exchanges/probable/api.d.ts +1 -1
- package/dist/exchanges/probable/api.js +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -1
- package/dist/server/app.js +1 -0
- package/dist/server/exchange-factory.js +8 -0
- package/dist/server/openapi.yaml +17 -0
- package/dist/types.d.ts +4 -0
- package/package.json +3 -3
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HUNCH_PROMOTED_MARKET_KEYS = exports.BASE_USDC_ADDRESS = exports.BASE_CHAIN_ID = exports.DEFAULT_BASE_URL = void 0;
|
|
4
|
+
exports.mapHunchStatus = mapHunchStatus;
|
|
5
|
+
exports.mapStatusToHunch = mapStatusToHunch;
|
|
6
|
+
exports.buildOutcomeId = buildOutcomeId;
|
|
7
|
+
exports.parseHunchSide = parseHunchSide;
|
|
8
|
+
exports.mapHunchMarketToUnified = mapHunchMarketToUnified;
|
|
9
|
+
const market_utils_1 = require("../../utils/market-utils");
|
|
10
|
+
const metadata_1 = require("../../utils/metadata");
|
|
11
|
+
/**
|
|
12
|
+
* Canonical Hunch agent-platform base URL. Reads are keyless (CORS `*`);
|
|
13
|
+
* the trade route settles real Base USDC via x402 / EIP-3009.
|
|
14
|
+
*/
|
|
15
|
+
exports.DEFAULT_BASE_URL = 'https://www.playhunch.xyz';
|
|
16
|
+
/** Base mainnet — the ONLY settlement chain on Hunch's agent rail. */
|
|
17
|
+
exports.BASE_CHAIN_ID = 8453;
|
|
18
|
+
/** USDC on Base (the EIP-3009 `transferWithAuthorization` asset). */
|
|
19
|
+
exports.BASE_USDC_ADDRESS = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
|
|
20
|
+
/**
|
|
21
|
+
* Raw Hunch fields already promoted to first-class Unified columns — excluded
|
|
22
|
+
* from `sourceMetadata` so we capture only what the unified shape would drop
|
|
23
|
+
* (e.g. headline, deadlineLabel, feeRecipientLabel, defaultTicketUsd, links).
|
|
24
|
+
*/
|
|
25
|
+
exports.HUNCH_PROMOTED_MARKET_KEYS = [
|
|
26
|
+
'id',
|
|
27
|
+
'slug',
|
|
28
|
+
'question',
|
|
29
|
+
'summary',
|
|
30
|
+
'category',
|
|
31
|
+
'tokenSymbol',
|
|
32
|
+
'chainId',
|
|
33
|
+
'deadlineAt',
|
|
34
|
+
'status',
|
|
35
|
+
'feeBps',
|
|
36
|
+
'virtualLiquidityUsd',
|
|
37
|
+
'volumeUsd',
|
|
38
|
+
'targetMarketCapUsd',
|
|
39
|
+
'outcomes',
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Map a Hunch status (open / closed / resolved) to the pmxt unified lifecycle
|
|
43
|
+
* vocabulary (active / inactive / closed). Unknown → 'active' (Hunch only lists
|
|
44
|
+
* `open` markets when `status=open`, so this is defensive).
|
|
45
|
+
*/
|
|
46
|
+
function mapHunchStatus(status) {
|
|
47
|
+
switch (status) {
|
|
48
|
+
case 'open':
|
|
49
|
+
return 'active';
|
|
50
|
+
case 'closed':
|
|
51
|
+
return 'inactive';
|
|
52
|
+
case 'resolved':
|
|
53
|
+
case 'voided':
|
|
54
|
+
return 'closed';
|
|
55
|
+
default:
|
|
56
|
+
return 'active';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Translate the pmxt unified `status` filter into the Hunch `status` query
|
|
61
|
+
* value. Hunch only accepts `open` | `all`; 'inactive'/'closed' have no live
|
|
62
|
+
* listing, so they fold to `all` (the caller filters client-side afterwards).
|
|
63
|
+
*/
|
|
64
|
+
function mapStatusToHunch(status) {
|
|
65
|
+
if (!status)
|
|
66
|
+
return undefined;
|
|
67
|
+
switch (status) {
|
|
68
|
+
case 'active':
|
|
69
|
+
return 'open';
|
|
70
|
+
case 'inactive':
|
|
71
|
+
case 'closed':
|
|
72
|
+
case 'all':
|
|
73
|
+
return 'all';
|
|
74
|
+
default:
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compose an outcome id that round-trips back to a Hunch trade `side`.
|
|
80
|
+
*
|
|
81
|
+
* Encoding: `${marketId}:${side}` where `side` is `yes` | `no` for binary
|
|
82
|
+
* markets, or the parimutuel bucket `key` (e.g. `le-330m`, `up`, `down`,
|
|
83
|
+
* a date-window key) for N-way markets. `parseHunchSide()` reverses it.
|
|
84
|
+
*/
|
|
85
|
+
function buildOutcomeId(marketId, side) {
|
|
86
|
+
return `${marketId}:${side}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reverse {@link buildOutcomeId}: pull the Hunch `side` token back out of a
|
|
90
|
+
* unified `outcomeId`. The market id itself may contain a `:` is NOT a concern —
|
|
91
|
+
* Hunch ids are slug-shaped (`[a-z0-9-]`), so the FIRST colon never appears
|
|
92
|
+
* inside an id; we split on the LAST colon to be safe regardless.
|
|
93
|
+
*/
|
|
94
|
+
function parseHunchSide(outcomeId) {
|
|
95
|
+
const idx = outcomeId.lastIndexOf(':');
|
|
96
|
+
if (idx <= 0 || idx === outcomeId.length - 1) {
|
|
97
|
+
// No separator — treat the whole thing as the side with no market id.
|
|
98
|
+
return { marketId: '', side: outcomeId };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
marketId: outcomeId.slice(0, idx),
|
|
102
|
+
side: outcomeId.slice(idx + 1),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Shared market normalizer used by both the live fetch path and the (rare)
|
|
107
|
+
* direct-mapping helper. Pulls a Hunch market ref into a {@link UnifiedMarket}.
|
|
108
|
+
*
|
|
109
|
+
* - Binary markets (`outcomes == null`) get YES/NO outcomes priced from `odds`
|
|
110
|
+
* when supplied (research/quote-derived), else flat 0 (the list endpoint
|
|
111
|
+
* does not carry live odds — they ride on the quote/research single-market
|
|
112
|
+
* reads, matching Myriad's "static on list, live on detail" model).
|
|
113
|
+
* - N-way markets expand `outcomes[]` into one {@link MarketOutcome} per rung,
|
|
114
|
+
* carrying `impliedPct/100` as the price when a live ladder is supplied.
|
|
115
|
+
*
|
|
116
|
+
* `outcomeId` round-trips to a Hunch `side` (see {@link buildOutcomeId}).
|
|
117
|
+
*/
|
|
118
|
+
function mapHunchMarketToUnified(raw, odds, ladder) {
|
|
119
|
+
if (!raw || !raw.id)
|
|
120
|
+
return null;
|
|
121
|
+
const marketId = raw.id;
|
|
122
|
+
let outcomes;
|
|
123
|
+
if (Array.isArray(raw.outcomes) && raw.outcomes.length > 0) {
|
|
124
|
+
// N-way parimutuel ladder / date-window market.
|
|
125
|
+
const ladderByKey = new Map();
|
|
126
|
+
for (const lo of ladder?.outcomes ?? [])
|
|
127
|
+
ladderByKey.set(lo.key, lo);
|
|
128
|
+
outcomes = raw.outcomes.map((o) => {
|
|
129
|
+
const live = ladderByKey.get(o.key);
|
|
130
|
+
const price = live && typeof live.impliedPct === 'number' ? live.impliedPct / 100 : 0;
|
|
131
|
+
return {
|
|
132
|
+
outcomeId: buildOutcomeId(marketId, o.key),
|
|
133
|
+
marketId,
|
|
134
|
+
label: o.label,
|
|
135
|
+
price,
|
|
136
|
+
metadata: {
|
|
137
|
+
key: o.key,
|
|
138
|
+
shortLabel: o.shortLabel,
|
|
139
|
+
lowerUsd: o.lowerUsd,
|
|
140
|
+
upperUsd: o.upperUsd,
|
|
141
|
+
startAt: o.startAt ?? null,
|
|
142
|
+
endAt: o.endAt ?? null,
|
|
143
|
+
backedUsd: live?.backedUsd ?? null,
|
|
144
|
+
isCurrent: live?.isCurrent ?? null,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Binary YES/NO market.
|
|
151
|
+
const yesPrice = odds && typeof odds.yesPriceCents === 'number' ? odds.yesPriceCents / 100 : 0;
|
|
152
|
+
const noPrice = odds && typeof odds.noPriceCents === 'number'
|
|
153
|
+
? odds.noPriceCents / 100
|
|
154
|
+
: yesPrice > 0
|
|
155
|
+
? 1 - yesPrice
|
|
156
|
+
: 0;
|
|
157
|
+
outcomes = [
|
|
158
|
+
{ outcomeId: buildOutcomeId(marketId, 'yes'), marketId, label: 'Yes', price: yesPrice },
|
|
159
|
+
{ outcomeId: buildOutcomeId(marketId, 'no'), marketId, label: 'No', price: noPrice },
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
const um = {
|
|
163
|
+
marketId,
|
|
164
|
+
title: raw.question || raw.shortTitle || '',
|
|
165
|
+
description: raw.summary || '',
|
|
166
|
+
slug: raw.slug,
|
|
167
|
+
outcomes,
|
|
168
|
+
resolutionDate: raw.deadlineAt ? new Date(raw.deadlineAt) : undefined,
|
|
169
|
+
// Hunch is parimutuel and reports no 24h volume split — surface 0.
|
|
170
|
+
volume24h: 0,
|
|
171
|
+
volume: typeof raw.volumeUsd === 'number' ? raw.volumeUsd : undefined,
|
|
172
|
+
liquidity: Number(raw.virtualLiquidityUsd || 0),
|
|
173
|
+
url: raw.links?.app || `${exports.DEFAULT_BASE_URL}/markets/${raw.slug || marketId}`,
|
|
174
|
+
category: raw.category,
|
|
175
|
+
tags: raw.tokenSymbol ? [raw.tokenSymbol] : [],
|
|
176
|
+
status: mapHunchStatus(raw.status),
|
|
177
|
+
sourceMetadata: (0, metadata_1.buildSourceMetadata)(raw, exports.HUNCH_PROMOTED_MARKET_KEYS),
|
|
178
|
+
};
|
|
179
|
+
// Standardize yes/no/up/down convenience accessors for binary markets.
|
|
180
|
+
(0, market_utils_1.addBinaryOutcomes)(um);
|
|
181
|
+
return um;
|
|
182
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { OrderBook, Trade } from '../../types';
|
|
2
|
+
export type FetchOrderBookFn = (id: string) => Promise<OrderBook>;
|
|
3
|
+
export type FetchTradesFn = (id: string, limit: number) => Promise<Trade[]>;
|
|
4
|
+
export declare class HunchWebSocket {
|
|
5
|
+
private readonly fetchOrderBook;
|
|
6
|
+
private readonly fetchTrades;
|
|
7
|
+
private readonly pollInterval;
|
|
8
|
+
private orderBookTimers;
|
|
9
|
+
private tradeTimers;
|
|
10
|
+
private orderBookResolvers;
|
|
11
|
+
private orderBookRejecters;
|
|
12
|
+
private tradeResolvers;
|
|
13
|
+
private tradeRejecters;
|
|
14
|
+
private seenTradeIds;
|
|
15
|
+
private orderBookFailureCount;
|
|
16
|
+
private tradeFailureCount;
|
|
17
|
+
private closed;
|
|
18
|
+
constructor(fetchOrderBook: FetchOrderBookFn, fetchTrades: FetchTradesFn, pollInterval?: number);
|
|
19
|
+
watchOrderBook(outcomeId: string): Promise<OrderBook>;
|
|
20
|
+
watchTrades(outcomeId: string): Promise<Trade[]>;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
private startOrderBookPolling;
|
|
23
|
+
private startTradePolling;
|
|
24
|
+
private handleFailure;
|
|
25
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HunchWebSocket = void 0;
|
|
4
|
+
const logger_1 = require("../../utils/logger");
|
|
5
|
+
/**
|
|
6
|
+
* Hunch exposes SSE at /api/agent/v1/events for resolution awareness, but no
|
|
7
|
+
* market-data WebSocket. watchOrderBook / watchTrades are emulated with a
|
|
8
|
+
* poll-based fallback (matching the CCXT Pro async pattern + Myriad's adapter).
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_POLL_INTERVAL = 5000; // 5s
|
|
11
|
+
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
12
|
+
class HunchWebSocket {
|
|
13
|
+
fetchOrderBook;
|
|
14
|
+
fetchTrades;
|
|
15
|
+
pollInterval;
|
|
16
|
+
orderBookTimers = new Map();
|
|
17
|
+
tradeTimers = new Map();
|
|
18
|
+
orderBookResolvers = new Map();
|
|
19
|
+
orderBookRejecters = new Map();
|
|
20
|
+
tradeResolvers = new Map();
|
|
21
|
+
tradeRejecters = new Map();
|
|
22
|
+
seenTradeIds = new Map();
|
|
23
|
+
orderBookFailureCount = new Map();
|
|
24
|
+
tradeFailureCount = new Map();
|
|
25
|
+
closed = false;
|
|
26
|
+
constructor(fetchOrderBook, fetchTrades, pollInterval) {
|
|
27
|
+
this.fetchOrderBook = fetchOrderBook;
|
|
28
|
+
this.fetchTrades = fetchTrades;
|
|
29
|
+
this.pollInterval = pollInterval || DEFAULT_POLL_INTERVAL;
|
|
30
|
+
}
|
|
31
|
+
async watchOrderBook(outcomeId) {
|
|
32
|
+
if (this.closed)
|
|
33
|
+
throw new Error('Hunch watch connection is closed');
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
if (!this.orderBookResolvers.has(outcomeId)) {
|
|
36
|
+
this.orderBookResolvers.set(outcomeId, []);
|
|
37
|
+
this.orderBookRejecters.set(outcomeId, []);
|
|
38
|
+
}
|
|
39
|
+
this.orderBookResolvers.get(outcomeId).push(resolve);
|
|
40
|
+
this.orderBookRejecters.get(outcomeId).push(reject);
|
|
41
|
+
if (!this.orderBookTimers.has(outcomeId))
|
|
42
|
+
this.startOrderBookPolling(outcomeId);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async watchTrades(outcomeId) {
|
|
46
|
+
if (this.closed)
|
|
47
|
+
throw new Error('Hunch watch connection is closed');
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
if (!this.tradeResolvers.has(outcomeId)) {
|
|
50
|
+
this.tradeResolvers.set(outcomeId, []);
|
|
51
|
+
this.tradeRejecters.set(outcomeId, []);
|
|
52
|
+
}
|
|
53
|
+
this.tradeResolvers.get(outcomeId).push(resolve);
|
|
54
|
+
this.tradeRejecters.get(outcomeId).push(reject);
|
|
55
|
+
if (!this.tradeTimers.has(outcomeId))
|
|
56
|
+
this.startTradePolling(outcomeId);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async close() {
|
|
60
|
+
this.closed = true;
|
|
61
|
+
for (const timer of this.orderBookTimers.values())
|
|
62
|
+
clearInterval(timer);
|
|
63
|
+
for (const timer of this.tradeTimers.values())
|
|
64
|
+
clearInterval(timer);
|
|
65
|
+
this.orderBookTimers.clear();
|
|
66
|
+
this.tradeTimers.clear();
|
|
67
|
+
this.orderBookResolvers.clear();
|
|
68
|
+
this.orderBookRejecters.clear();
|
|
69
|
+
this.tradeResolvers.clear();
|
|
70
|
+
this.tradeRejecters.clear();
|
|
71
|
+
this.seenTradeIds.clear();
|
|
72
|
+
}
|
|
73
|
+
startOrderBookPolling(id) {
|
|
74
|
+
const poll = async () => {
|
|
75
|
+
try {
|
|
76
|
+
const book = await this.fetchOrderBook(id);
|
|
77
|
+
this.orderBookFailureCount.set(id, 0);
|
|
78
|
+
const resolvers = this.orderBookResolvers.get(id) || [];
|
|
79
|
+
this.orderBookResolvers.set(id, []);
|
|
80
|
+
this.orderBookRejecters.set(id, []);
|
|
81
|
+
for (const resolve of resolvers)
|
|
82
|
+
resolve(book);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
this.handleFailure(id, error, 'watchOrderBook', this.orderBookFailureCount, this.orderBookTimers, this.orderBookResolvers, this.orderBookRejecters);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
poll();
|
|
89
|
+
this.orderBookTimers.set(id, setInterval(poll, this.pollInterval));
|
|
90
|
+
}
|
|
91
|
+
startTradePolling(id) {
|
|
92
|
+
const poll = async () => {
|
|
93
|
+
try {
|
|
94
|
+
const trades = await this.fetchTrades(id, 50);
|
|
95
|
+
let seen = this.seenTradeIds.get(id);
|
|
96
|
+
if (!seen) {
|
|
97
|
+
seen = new Set();
|
|
98
|
+
this.seenTradeIds.set(id, seen);
|
|
99
|
+
}
|
|
100
|
+
const fresh = trades.filter((t) => !seen.has(t.id));
|
|
101
|
+
for (const t of fresh)
|
|
102
|
+
seen.add(t.id);
|
|
103
|
+
this.tradeFailureCount.set(id, 0);
|
|
104
|
+
const resolvers = this.tradeResolvers.get(id) || [];
|
|
105
|
+
this.tradeResolvers.set(id, []);
|
|
106
|
+
this.tradeRejecters.set(id, []);
|
|
107
|
+
for (const resolve of resolvers)
|
|
108
|
+
resolve(fresh);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.handleFailure(id, error, 'watchTrades', this.tradeFailureCount, this.tradeTimers, this.tradeResolvers, this.tradeRejecters);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
poll();
|
|
115
|
+
this.tradeTimers.set(id, setInterval(poll, this.pollInterval));
|
|
116
|
+
}
|
|
117
|
+
handleFailure(id, error, label, failures, timers, resolvers, rejecters) {
|
|
118
|
+
const count = (failures.get(id) || 0) + 1;
|
|
119
|
+
failures.set(id, count);
|
|
120
|
+
logger_1.logger.warn(`Hunch ${label} poll failed for outcomeId=${id} (consecutive failures: ${count})`, {
|
|
121
|
+
error: String(error),
|
|
122
|
+
});
|
|
123
|
+
if (count >= MAX_CONSECUTIVE_FAILURES) {
|
|
124
|
+
const timer = timers.get(id);
|
|
125
|
+
if (timer)
|
|
126
|
+
clearInterval(timer);
|
|
127
|
+
timers.delete(id);
|
|
128
|
+
failures.delete(id);
|
|
129
|
+
const rej = rejecters.get(id) || [];
|
|
130
|
+
resolvers.set(id, []);
|
|
131
|
+
rejecters.set(id, []);
|
|
132
|
+
for (const reject of rej)
|
|
133
|
+
reject(error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.HunchWebSocket = HunchWebSocket;
|
|
@@ -46,12 +46,25 @@ function convertLargeInts(obj) {
|
|
|
46
46
|
}
|
|
47
47
|
return obj;
|
|
48
48
|
}
|
|
49
|
+
// ponytail: msgpackr encodes positive BigInts as int64 (0xd3); HL's server uses
|
|
50
|
+
// Python's msgpack which encodes the same value as uint64 (0xcf). Post-process the
|
|
51
|
+
// packed bytes to flip d3 → cf for non-negative payloads. HL actions never carry
|
|
52
|
+
// negative integers (oids, nonces, asset ids are all >= 0), so this is safe.
|
|
53
|
+
function fixInt64ToUint64(bytes) {
|
|
54
|
+
const out = Buffer.from(bytes);
|
|
55
|
+
for (let i = 0; i < out.length - 8; i++) {
|
|
56
|
+
if (out[i] === 0xd3 && (out[i + 1] & 0x80) === 0) {
|
|
57
|
+
out[i] = 0xcf;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
49
62
|
// ----------------------------------------------------------------------------
|
|
50
63
|
// Action hash -- constructs the connectionId for the phantom agent
|
|
51
64
|
// ----------------------------------------------------------------------------
|
|
52
65
|
function computeActionHash(action, vaultAddress, nonce) {
|
|
53
|
-
// 1. msgpack-encode the action
|
|
54
|
-
const actionBytes = packr.pack(convertLargeInts(action));
|
|
66
|
+
// 1. msgpack-encode the action; coerce positive int64 to uint64 to match HL's server (Python msgpack)
|
|
67
|
+
const actionBytes = fixInt64ToUint64(packr.pack(convertLargeInts(action)));
|
|
55
68
|
// 2. nonce as 8 bytes big-endian
|
|
56
69
|
const nonceBytes = Buffer.alloc(8);
|
|
57
70
|
nonceBytes.writeBigUInt64BE(BigInt(nonce));
|
|
@@ -126,10 +126,28 @@ export interface HyperliquidRawUserState {
|
|
|
126
126
|
time?: number;
|
|
127
127
|
withdrawable: string;
|
|
128
128
|
}
|
|
129
|
+
export interface HyperliquidRawSpotBalance {
|
|
130
|
+
coin: string;
|
|
131
|
+
token: number;
|
|
132
|
+
total: string;
|
|
133
|
+
hold: string;
|
|
134
|
+
entryNtl: string;
|
|
135
|
+
}
|
|
136
|
+
export interface HyperliquidRawSpotState {
|
|
137
|
+
balances: HyperliquidRawSpotBalance[];
|
|
138
|
+
}
|
|
139
|
+
export interface HyperliquidRawSpotAssetCtx {
|
|
140
|
+
coin: string;
|
|
141
|
+
dayNtlVlm: string;
|
|
142
|
+
prevDayPx?: string;
|
|
143
|
+
markPx?: string;
|
|
144
|
+
midPx?: string;
|
|
145
|
+
}
|
|
129
146
|
export interface HyperliquidRawOutcomeWithQuestion {
|
|
130
147
|
outcome: HyperliquidRawOutcome;
|
|
131
148
|
question: HyperliquidRawQuestion | undefined;
|
|
132
149
|
midPrice: string | undefined;
|
|
150
|
+
volume24h?: number;
|
|
133
151
|
}
|
|
134
152
|
export declare class HyperliquidFetcher implements IExchangeFetcher<HyperliquidRawOutcomeWithQuestion, HyperliquidRawQuestion> {
|
|
135
153
|
private readonly ctx;
|
|
@@ -140,11 +158,18 @@ export declare class HyperliquidFetcher implements IExchangeFetcher<HyperliquidR
|
|
|
140
158
|
fetchRawEvents(params: EventFetchParams): Promise<HyperliquidRawQuestion[]>;
|
|
141
159
|
fetchRawOrderBook(marketId: string): Promise<HyperliquidRawL2Book>;
|
|
142
160
|
fetchRawOHLCV(marketId: string, params: OHLCVParams): Promise<HyperliquidRawCandle[]>;
|
|
143
|
-
fetchRawTrades(marketId: string,
|
|
161
|
+
fetchRawTrades(marketId: string, params: TradesParams): Promise<HyperliquidRawTrade[]>;
|
|
144
162
|
fetchRawUserFills(walletAddress: string): Promise<HyperliquidRawFill[]>;
|
|
145
163
|
fetchRawOpenOrders(walletAddress: string): Promise<HyperliquidRawOpenOrder[]>;
|
|
164
|
+
fetchRawSpotState(walletAddress: string): Promise<HyperliquidRawSpotState>;
|
|
146
165
|
fetchRawUserState(walletAddress: string): Promise<HyperliquidRawUserState>;
|
|
147
166
|
fetchOutcomeMeta(): Promise<HyperliquidRawOutcomeMeta>;
|
|
148
167
|
fetchAllMids(): Promise<HyperliquidRawMid>;
|
|
168
|
+
/**
|
|
169
|
+
* Build a map of outcomeId -> 24h notional volume (Yes leg + No leg)
|
|
170
|
+
* by reading spotMetaAndAssetCtxs, where outcome legs appear as
|
|
171
|
+
* coin "#<encoding>" with `dayNtlVlm` in USDC.
|
|
172
|
+
*/
|
|
173
|
+
fetchOutcomeVolumeMap(): Promise<Map<number, number>>;
|
|
149
174
|
private getMidForOutcome;
|
|
150
175
|
}
|
|
@@ -25,9 +25,10 @@ class HyperliquidFetcher {
|
|
|
25
25
|
}
|
|
26
26
|
// -- Markets (outcomes) ----------------------------------------------------
|
|
27
27
|
async fetchRawMarkets(params) {
|
|
28
|
-
const [meta, mids] = await Promise.all([
|
|
28
|
+
const [meta, mids, volumeMap] = await Promise.all([
|
|
29
29
|
this.fetchOutcomeMeta(),
|
|
30
30
|
this.fetchAllMids(),
|
|
31
|
+
this.fetchOutcomeVolumeMap(),
|
|
31
32
|
]);
|
|
32
33
|
const questionMap = new Map();
|
|
33
34
|
for (const q of meta.questions) {
|
|
@@ -39,6 +40,7 @@ class HyperliquidFetcher {
|
|
|
39
40
|
outcome,
|
|
40
41
|
question: questionMap.get(outcome.outcome),
|
|
41
42
|
midPrice: this.getMidForOutcome(mids, outcome.outcome),
|
|
43
|
+
volume24h: volumeMap.get(outcome.outcome),
|
|
42
44
|
}));
|
|
43
45
|
// Filter settled outcomes out by default (active only)
|
|
44
46
|
if (!params?.status || params.status === 'active') {
|
|
@@ -56,6 +58,21 @@ class HyperliquidFetcher {
|
|
|
56
58
|
results = results.filter(r => r.outcome.name.toLowerCase().includes(lowerQuery) ||
|
|
57
59
|
r.outcome.description.toLowerCase().includes(lowerQuery));
|
|
58
60
|
}
|
|
61
|
+
// Direct lookup by marketId (canonical or outcome-token form)
|
|
62
|
+
if (params?.marketId) {
|
|
63
|
+
try {
|
|
64
|
+
const targetOutcomeId = (0, utils_1.fromMarketId)(String(params.marketId));
|
|
65
|
+
results = results.filter(r => r.outcome.outcome === targetOutcomeId);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
results = [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Filter by parent eventId (HL question number)
|
|
72
|
+
if (params?.eventId !== undefined && params.eventId !== null) {
|
|
73
|
+
const targetEventId = String(params.eventId);
|
|
74
|
+
results = results.filter(r => r.question && String(r.question.question) === targetEventId);
|
|
75
|
+
}
|
|
59
76
|
// Limit
|
|
60
77
|
const limit = params?.limit || 250000;
|
|
61
78
|
const offset = params?.offset || 0;
|
|
@@ -65,6 +82,11 @@ class HyperliquidFetcher {
|
|
|
65
82
|
async fetchRawEvents(params) {
|
|
66
83
|
const meta = await this.fetchOutcomeMeta();
|
|
67
84
|
let results = [...meta.questions];
|
|
85
|
+
// Direct lookup by eventId (HL question number)
|
|
86
|
+
if (params?.eventId !== undefined && params.eventId !== null) {
|
|
87
|
+
const targetEventId = String(params.eventId);
|
|
88
|
+
results = results.filter(q => String(q.question) === targetEventId);
|
|
89
|
+
}
|
|
68
90
|
// Filter by query
|
|
69
91
|
if (params?.query) {
|
|
70
92
|
const lowerQuery = params.query.toLowerCase();
|
|
@@ -92,16 +114,20 @@ class HyperliquidFetcher {
|
|
|
92
114
|
const now = Date.now();
|
|
93
115
|
const startTime = params.start ? params.start.getTime() : now - 24 * 60 * 60 * 1000;
|
|
94
116
|
const endTime = params.end ? params.end.getTime() : now;
|
|
95
|
-
|
|
117
|
+
const raw = await this.postInfo({
|
|
96
118
|
type: 'candleSnapshot',
|
|
97
119
|
req: { coin, interval: params.resolution || '1h', startTime, endTime },
|
|
98
120
|
});
|
|
121
|
+
// ponytail: HL returns the full window; honor caller's limit by trimming to the most recent N
|
|
122
|
+
return params.limit && raw.length > params.limit ? raw.slice(-params.limit) : raw;
|
|
99
123
|
}
|
|
100
124
|
// -- Trades ----------------------------------------------------------------
|
|
101
|
-
async fetchRawTrades(marketId,
|
|
125
|
+
async fetchRawTrades(marketId, params) {
|
|
102
126
|
const outcomeId = (0, utils_1.fromMarketId)(marketId);
|
|
103
127
|
const coin = (0, utils_1.toCoinNotation)(outcomeId, 'yes');
|
|
104
|
-
|
|
128
|
+
const raw = await this.postInfo({ type: 'recentTrades', coin });
|
|
129
|
+
// ponytail: HL recentTrades returns a fixed page; honor caller's limit (most recent N)
|
|
130
|
+
return params?.limit && raw.length > params.limit ? raw.slice(0, params.limit) : raw;
|
|
105
131
|
}
|
|
106
132
|
// -- User data -------------------------------------------------------------
|
|
107
133
|
async fetchRawUserFills(walletAddress) {
|
|
@@ -116,6 +142,12 @@ class HyperliquidFetcher {
|
|
|
116
142
|
user: walletAddress,
|
|
117
143
|
});
|
|
118
144
|
}
|
|
145
|
+
async fetchRawSpotState(walletAddress) {
|
|
146
|
+
return this.postInfo({
|
|
147
|
+
type: 'spotClearinghouseState',
|
|
148
|
+
user: walletAddress,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
119
151
|
async fetchRawUserState(walletAddress) {
|
|
120
152
|
return this.postInfo({
|
|
121
153
|
type: 'clearinghouseState',
|
|
@@ -129,6 +161,36 @@ class HyperliquidFetcher {
|
|
|
129
161
|
async fetchAllMids() {
|
|
130
162
|
return this.postInfo({ type: 'allMids' });
|
|
131
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Build a map of outcomeId -> 24h notional volume (Yes leg + No leg)
|
|
166
|
+
* by reading spotMetaAndAssetCtxs, where outcome legs appear as
|
|
167
|
+
* coin "#<encoding>" with `dayNtlVlm` in USDC.
|
|
168
|
+
*/
|
|
169
|
+
async fetchOutcomeVolumeMap() {
|
|
170
|
+
const map = new Map();
|
|
171
|
+
try {
|
|
172
|
+
const resp = await this.postInfo({ type: 'spotMetaAndAssetCtxs' });
|
|
173
|
+
const ctxs = Array.isArray(resp) ? resp[1] : undefined;
|
|
174
|
+
if (!Array.isArray(ctxs))
|
|
175
|
+
return map;
|
|
176
|
+
for (const ctx of ctxs) {
|
|
177
|
+
if (!ctx?.coin || !ctx.coin.startsWith('#'))
|
|
178
|
+
continue;
|
|
179
|
+
const vol = parseFloat(ctx.dayNtlVlm);
|
|
180
|
+
if (!Number.isFinite(vol))
|
|
181
|
+
continue;
|
|
182
|
+
const encoding = parseInt(ctx.coin.slice(1), 10);
|
|
183
|
+
if (!Number.isFinite(encoding))
|
|
184
|
+
continue;
|
|
185
|
+
const { outcomeId } = (0, utils_1.fromCoinEncoding)(encoding);
|
|
186
|
+
map.set(outcomeId, (map.get(outcomeId) ?? 0) + vol);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// ponytail: best-effort volume enrichment; if spotMetaAndAssetCtxs is unreachable, return empty map and callers fall back to 0
|
|
191
|
+
}
|
|
192
|
+
return map;
|
|
193
|
+
}
|
|
132
194
|
getMidForOutcome(mids, outcomeId) {
|
|
133
195
|
const midKey = (0, utils_1.toMidKey)(outcomeId);
|
|
134
196
|
return mids[midKey];
|
|
@@ -20,12 +20,15 @@ export declare class HyperliquidExchange extends PredictionMarketExchange {
|
|
|
20
20
|
protected fetchMarketsImpl(params?: MarketFilterParams): Promise<UnifiedMarket[]>;
|
|
21
21
|
protected fetchEventsImpl(params: EventFetchParams): Promise<UnifiedEvent[]>;
|
|
22
22
|
fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook>;
|
|
23
|
-
|
|
23
|
+
fetchOrderBooks(outcomeIds: string[]): Promise<Record<string, OrderBook>>;
|
|
24
|
+
fetchOHLCV(outcomeId: string, params?: OHLCVParams): Promise<PriceCandle[]>;
|
|
24
25
|
fetchTrades(outcomeId: string, params?: TradesParams): Promise<Trade[]>;
|
|
25
26
|
fetchBalance(): Promise<Balance[]>;
|
|
26
27
|
fetchPositions(): Promise<Position[]>;
|
|
27
28
|
fetchOpenOrders(): Promise<Order[]>;
|
|
28
29
|
fetchMyTrades(params?: MyTradesParams): Promise<UserTrade[]>;
|
|
30
|
+
fetchClosedOrders(): Promise<Order[]>;
|
|
31
|
+
fetchAllOrders(): Promise<Order[]>;
|
|
29
32
|
buildOrder(params: CreateOrderParams): Promise<BuiltOrder>;
|
|
30
33
|
submitOrder(built: BuiltOrder): Promise<Order>;
|
|
31
34
|
createOrder(params: CreateOrderParams): Promise<Order>;
|