pmxt-core 2.19.6 → 2.20.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 (57) hide show
  1. package/dist/BaseExchange.d.ts +69 -30
  2. package/dist/BaseExchange.js +124 -82
  3. package/dist/exchanges/baozi/index.d.ts +2 -0
  4. package/dist/exchanges/baozi/index.js +2 -0
  5. package/dist/exchanges/baozi/price.d.ts +3 -0
  6. package/dist/exchanges/baozi/price.js +16 -0
  7. package/dist/exchanges/baozi/price.test.d.ts +1 -0
  8. package/dist/exchanges/baozi/price.test.js +33 -0
  9. package/dist/exchanges/baozi/utils.js +5 -9
  10. package/dist/exchanges/kalshi/api.d.ts +1 -1
  11. package/dist/exchanges/kalshi/api.js +1 -1
  12. package/dist/exchanges/kalshi/fetchOHLCV.js +5 -4
  13. package/dist/exchanges/kalshi/fetchOrderBook.js +21 -21
  14. package/dist/exchanges/kalshi/fetchTrades.js +2 -1
  15. package/dist/exchanges/kalshi/index.d.ts +3 -1
  16. package/dist/exchanges/kalshi/index.js +19 -16
  17. package/dist/exchanges/kalshi/price.d.ts +3 -0
  18. package/dist/exchanges/kalshi/price.js +14 -0
  19. package/dist/exchanges/kalshi/price.test.d.ts +1 -0
  20. package/dist/exchanges/kalshi/price.test.js +24 -0
  21. package/dist/exchanges/kalshi/utils.js +5 -4
  22. package/dist/exchanges/limitless/api.d.ts +1 -1
  23. package/dist/exchanges/limitless/api.js +1 -1
  24. package/dist/exchanges/limitless/index.d.ts +58 -19
  25. package/dist/exchanges/limitless/index.js +169 -101
  26. package/dist/exchanges/limitless/websocket.d.ts +10 -3
  27. package/dist/exchanges/limitless/websocket.js +71 -52
  28. package/dist/exchanges/myriad/api.d.ts +1 -1
  29. package/dist/exchanges/myriad/api.js +1 -1
  30. package/dist/exchanges/myriad/index.d.ts +3 -1
  31. package/dist/exchanges/myriad/index.js +7 -4
  32. package/dist/exchanges/myriad/price.d.ts +1 -0
  33. package/dist/exchanges/myriad/price.js +7 -0
  34. package/dist/exchanges/myriad/price.test.d.ts +1 -0
  35. package/dist/exchanges/myriad/price.test.js +17 -0
  36. package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
  37. package/dist/exchanges/polymarket/api-clob.js +1 -1
  38. package/dist/exchanges/polymarket/api-data.d.ts +1 -1
  39. package/dist/exchanges/polymarket/api-data.js +1 -1
  40. package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
  41. package/dist/exchanges/polymarket/api-gamma.js +1 -1
  42. package/dist/exchanges/polymarket/index.d.ts +28 -15
  43. package/dist/exchanges/polymarket/index.js +217 -137
  44. package/dist/exchanges/polymarket/websocket.d.ts +11 -4
  45. package/dist/exchanges/polymarket/websocket.js +58 -36
  46. package/dist/exchanges/probable/api.d.ts +1 -1
  47. package/dist/exchanges/probable/api.js +1 -1
  48. package/dist/exchanges/probable/index.d.ts +2 -0
  49. package/dist/exchanges/probable/index.js +2 -0
  50. package/dist/subscriber/base.d.ts +82 -0
  51. package/dist/subscriber/base.js +2 -0
  52. package/dist/subscriber/external/goldsky.d.ts +96 -0
  53. package/dist/subscriber/external/goldsky.js +412 -0
  54. package/dist/subscriber/watcher.d.ts +85 -0
  55. package/dist/subscriber/watcher.js +178 -0
  56. package/dist/types.d.ts +5 -0
  57. package/package.json +3 -3
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.probableApiSpec = void 0;
4
4
  /**
5
5
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/probable/probable.yaml
6
- * Generated at: 2026-03-06T09:16:55.599Z
6
+ * Generated at: 2026-03-14T16:22:11.429Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.probableApiSpec = {
@@ -14,6 +14,8 @@ export declare class ProbableExchange extends PredictionMarketExchange {
14
14
  fetchOpenOrders: true;
15
15
  fetchPositions: true;
16
16
  fetchBalance: true;
17
+ watchAddress: false;
18
+ unwatchAddress: false;
17
19
  watchOrderBook: true;
18
20
  watchTrades: false;
19
21
  fetchMyTrades: true;
@@ -45,6 +45,8 @@ class ProbableExchange extends BaseExchange_1.PredictionMarketExchange {
45
45
  fetchOpenOrders: true,
46
46
  fetchPositions: true,
47
47
  fetchBalance: true,
48
+ watchAddress: false,
49
+ unwatchAddress: false,
48
50
  watchOrderBook: true,
49
51
  watchTrades: false,
50
52
  fetchMyTrades: true,
@@ -0,0 +1,82 @@
1
+ import { Balance, Position, Trade } from '../types';
2
+ export type SubscriptionOption = 'trades' | 'positions' | 'balances';
3
+ export interface SubscribedAddressSnapshot {
4
+ /** The wallet address being watched */
5
+ address: string;
6
+ /** Recent trades for this address
7
+ * (if the above SubscriptionOption 'trades' option was requested)
8
+ */
9
+ trades?: Trade[];
10
+ /** Current open positions for this address
11
+ * (if the above SubscriptionOption 'positions' option was requested)
12
+ */
13
+ positions?: Position[];
14
+ /** Current balances for this address
15
+ * (if the above SubscriptionOption 'balances' option was requested)
16
+ */
17
+ balances?: Balance[];
18
+ /** Unix timestamp (ms) of this snapshot */
19
+ timestamp: number;
20
+ }
21
+ /**
22
+ * Partial snapshot constructed from a subscribed event's on-chain data.
23
+ * Only the types that could be fully derived from the event are present.
24
+ */
25
+ export type SubscribedResult = Partial<Omit<SubscribedAddressSnapshot, 'address' | 'timestamp'>>;
26
+ type SubscriptionBuilder = (address: string) => any;
27
+ /**
28
+ * Tries to build a partial SubscribedAddressSnapshot from raw watched event data.
29
+ * The implementation varies depending on the implementation of SubscriptionBuilder.
30
+ *
31
+ * Data is the raw payload, the subscribed address, the requested types,
32
+ * and the last known snapshot.
33
+ *
34
+ * Return an `SubscribedResult` object containing only the types you can fully
35
+ * populate from the event
36
+ */
37
+ export type SubscribedActivityBuilder = (data: unknown, address: string, types: SubscriptionOption[], lastSnapshot?: SubscribedAddressSnapshot | null) => SubscribedResult | null;
38
+ export interface SubscriberConfig {
39
+ /**
40
+ * HTTP endpoint used for polling queries.
41
+ */
42
+ baseUrl?: string;
43
+ /**
44
+ * Milliseconds between query polls once websocket subscription is not available.
45
+ * @default 3000
46
+ */
47
+ pollMs?: number;
48
+ /**
49
+ * WebSocket endpoint
50
+ */
51
+ wsEndpoint?: string;
52
+ /** API key for authenticating with the external subscription provider.
53
+ * Required when the provider restricts access to authenticated clients.
54
+ */
55
+ apiKey?: string;
56
+ /**
57
+ * Builds the customized per-address subscription query
58
+ */
59
+ buildSubscription?: SubscriptionBuilder;
60
+ /**
61
+ * Milliseconds between reconnect attempts after a WebSocket disconnect.
62
+ * @default 5000
63
+ */
64
+ reconnectDelayMs?: number;
65
+ }
66
+ /**
67
+ * Optional subscription that notifies the watcher of on-chain activity for a
68
+ * watched address.
69
+ */
70
+ export interface BaseSubscriber {
71
+ /**
72
+ * Start receiving notifications for `address`.
73
+ * Resolves once the subscription is active, or throws if the watcher
74
+ * cannot be set up (the watcher will fall back to polling-only on error).
75
+ */
76
+ subscribe(address: string, types: SubscriptionOption[], onEvent: (data: unknown) => void): Promise<void>;
77
+ /** Stop receiving notifications for `address`. */
78
+ unsubscribe(address: string): void;
79
+ /** Tear down all subscriptions and close underlying connections. */
80
+ close(): void;
81
+ }
82
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,96 @@
1
+ import { BaseSubscriber, SubscribedActivityBuilder, SubscriberConfig, SubscriptionOption } from '../base';
2
+ /**
3
+ * A single GraphQL query to send to a Goldsky subgraph url.
4
+ */
5
+ export interface GoldSkyGraphQlQuery {
6
+ url: string;
7
+ query: string;
8
+ variables?: Record<string, any>;
9
+ }
10
+ /**
11
+ * Executes a single GraphQL query and returns the `data` object, or `null` on
12
+ * error. Provided by `GoldSkySubscriber` to each builder.
13
+ */
14
+ export type GoldSkyFetch = (query: GoldSkyGraphQlQuery) => Promise<Record<string, unknown> | null>;
15
+ /**
16
+ * Async builder that orchestrates one or more GraphQL queries and returns the
17
+ * merged result.
18
+ *
19
+ * - Receives a `fetch` helper so it can chain requests sequentially or run
20
+ * them in parallel as needed.
21
+ * - Return `null` when the requested `types` don't match what this builder
22
+ * covers (polling will be skipped for that address).
23
+ * - Return an empty object `{}` when types match but the current query yields
24
+ * no results (however, polling continues and waiting for future changes).
25
+ */
26
+ export type GoldSkySubscriptionBuilder = (address: string, types: SubscriptionOption[], fetch: GoldSkyFetch, baseUrl?: string) => Promise<Record<string, unknown> | null>;
27
+ export interface GoldSkyConfig extends Omit<SubscriberConfig, 'buildSubscription'> {
28
+ buildSubscription: GoldSkySubscriptionBuilder;
29
+ }
30
+ /**
31
+ * Polymarket combined subscription.
32
+ *
33
+ * - `'trades'`: two parallel indexed queries (maker + taker), merged and sorted
34
+ * by timestamp in the builder. The combined `or` filter causes a full-table
35
+ * scan and times out.
36
+ * - `'positions'`: PNL and positions metadata run sequentially.
37
+ * Both use `orderBy: id` to avoid timeouts on unindexed sort columns; the
38
+ * builder re-sorts by `amount desc` after fetching.
39
+ *
40
+ * Pair with `buildPolymarketActivity`.
41
+ */
42
+ export declare const POLYMARKET_DEFAULT_SUBSCRIPTION: GoldSkySubscriptionBuilder;
43
+ /**
44
+ * Limitless: watches ERC-20 `Transfer` events on the USDC contract.
45
+ * Only active when `'balances'` is in the requested types.
46
+ *
47
+ * Pair with `buildLimitlessBalanceActivity`.
48
+ */
49
+ export declare const LIMITLESS_DEFAULT_SUBSCRIPTION: GoldSkySubscriptionBuilder;
50
+ /**
51
+ * Derives `Trade[]` from Polymarket CTF Exchange `OrderFilled` event data.
52
+ */
53
+ export declare const buildPolymarketTradesActivity: SubscribedActivityBuilder;
54
+ /**
55
+ * Derives `Position[]` from the joined PNL + positions-metadata event data.
56
+ *
57
+ * `userPositions` (PNL) and `userBalances` (positions) are guaranteed
58
+ * to share the same tokenIds since step 2 is filtered by step 1's results.
59
+ *
60
+ * `currentPrice` and `unrealizedPnL` are left at 0 (not available on-chain).
61
+ */
62
+ /**
63
+ * Derives `Position[]` from joined PNL (`userPositions`) + metadata (`userBalances`).
64
+ * `currentPrice` and `unrealizedPnL` are left at 0 (not available on-chain).
65
+ */
66
+ export declare const buildPolymarketPositionsActivity: SubscribedActivityBuilder;
67
+ /**
68
+ * Combined activity builder for Polymarket. Pair with `POLYMARKET_DEFAULT_SUBSCRIPTION`.
69
+ */
70
+ export declare const buildPolymarketActivity: SubscribedActivityBuilder;
71
+ /**
72
+ * Derives a USDC balance delta from a Limitless ERC-20 transfer event.
73
+ * Returns `null` to fall back to full RPC fetch when baseline is missing.
74
+ */
75
+ export declare const buildLimitlessBalanceActivity: SubscribedActivityBuilder;
76
+ /**
77
+ * Polls goldsky subgraph endpoints on a configurable interval.
78
+ *
79
+ * Passes a `GoldSkyFetch` helper to each builder invocation, allowing builders
80
+ * to chain requests sequentially or run them in parallel as needed.
81
+ */
82
+ export declare class GoldSkySubscriber implements BaseSubscriber {
83
+ readonly config: GoldSkyConfig;
84
+ private readonly pollMs;
85
+ private abortControllers;
86
+ private pollTimers;
87
+ private callbacks;
88
+ private addressQueryTypes;
89
+ private closed;
90
+ constructor(config: GoldSkyConfig);
91
+ subscribe(address: string, types: SubscriptionOption[], onEvent: (data: unknown) => void): Promise<void>;
92
+ unsubscribe(address: string): void;
93
+ close(): void;
94
+ private query;
95
+ private runQuery;
96
+ }
@@ -0,0 +1,412 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GoldSkySubscriber = exports.buildLimitlessBalanceActivity = exports.buildPolymarketActivity = exports.buildPolymarketPositionsActivity = exports.buildPolymarketTradesActivity = exports.LIMITLESS_DEFAULT_SUBSCRIPTION = exports.POLYMARKET_DEFAULT_SUBSCRIPTION = void 0;
4
+ // ----------------------------------------------------------------------------
5
+ // Polymarket endpoints
6
+ // ----------------------------------------------------------------------------
7
+ // Reference: https://docs.polymarket.com/market-data/subgraph
8
+ const POLYMARKET_TRADES_ENDPOINT = 'https://api.goldsky.com/api/public/project_cl6mb8i9h0003e201j6li0diw/subgraphs/orderbook-subgraph/prod/gn';
9
+ const POLYMARKET_POSITIONS_ENDPOINT = 'https://api.goldsky.com/api/public/project_cl6mb8i9h0003e201j6li0diw/subgraphs/positions-subgraph/0.0.7/gn';
10
+ const POLYMARKET_PNL_ENDPOINT = 'https://api.goldsky.com/api/public/project_cl6mb8i9h0003e201j6li0diw/subgraphs/pnl-subgraph/0.0.14/gn';
11
+ // NOTE: orderBy must use `id` (primary key) on pnl-subgraph and positions-subgraph.
12
+ // Sorting by any unindexed column (e.g. amount, balance) causes a statement timeout.
13
+ // ----------------------------------------------------------------------------
14
+ // Internal query builders
15
+ // ----------------------------------------------------------------------------
16
+ const TRADES_FIELDS = `
17
+ id
18
+ timestamp
19
+ maker
20
+ taker
21
+ makerAssetId
22
+ takerAssetId
23
+ makerAmountFilled
24
+ takerAmountFilled`;
25
+ const BUILD_POLYMARKET_TRADES_AS_MAKER_QUERY = (address, url) => ({
26
+ url: url ?? POLYMARKET_TRADES_ENDPOINT,
27
+ query: `
28
+ query GetPolymarketTradesMaker($address: Bytes!) {
29
+ orderFilledEvents(
30
+ where: { maker: $address }
31
+ first: 5
32
+ orderBy: timestamp
33
+ orderDirection: desc
34
+ ) {${TRADES_FIELDS}
35
+ }
36
+ }
37
+ `,
38
+ variables: { address: address.toLowerCase() },
39
+ });
40
+ const BUILD_POLYMARKET_TRADES_AS_TAKER_QUERY = (address, url) => ({
41
+ url: url ?? POLYMARKET_TRADES_ENDPOINT,
42
+ query: `
43
+ query GetPolymarketTradesTaker($address: Bytes!) {
44
+ orderFilledEvents(
45
+ where: { taker: $address }
46
+ first: 20
47
+ orderBy: timestamp
48
+ orderDirection: desc
49
+ ) {${TRADES_FIELDS}
50
+ }
51
+ }
52
+ `,
53
+ variables: { address: address.toLowerCase() },
54
+ });
55
+ const BUILD_POLYMARKET_PNL_QUERY = (address, url) => ({
56
+ url: url ?? POLYMARKET_PNL_ENDPOINT,
57
+ query: `
58
+ query GetPolymarketPnl($address: String!) {
59
+ userPositions(
60
+ where: { user: $address, amount_gt: "0" }
61
+ first: 1000
62
+ orderBy: id
63
+ orderDirection: asc
64
+ ) {
65
+ tokenId
66
+ amount
67
+ avgPrice
68
+ realizedPnl
69
+ }
70
+ }
71
+ `,
72
+ variables: { address: address.toLowerCase() },
73
+ });
74
+ const BUILD_POLYMARKET_POSITIONS_QUERY = (_address, tokenIds, url) => ({
75
+ url: url ?? POLYMARKET_POSITIONS_ENDPOINT,
76
+ query: `
77
+ query GetPolymarketPositions($tokenIds: [ID!]!) {
78
+ tokenIdConditions(
79
+ where: { id_in: $tokenIds }
80
+ first: 1000
81
+ orderBy: id
82
+ orderDirection: asc
83
+ ) {
84
+ id
85
+ outcomeIndex
86
+ condition {
87
+ id
88
+ }
89
+ }
90
+ }
91
+ `,
92
+ variables: { tokenIds },
93
+ });
94
+ // ----------------------------------------------------------------------------
95
+ // Exported subscription builders
96
+ // ----------------------------------------------------------------------------
97
+ /**
98
+ * Polymarket combined subscription.
99
+ *
100
+ * - `'trades'`: two parallel indexed queries (maker + taker), merged and sorted
101
+ * by timestamp in the builder. The combined `or` filter causes a full-table
102
+ * scan and times out.
103
+ * - `'positions'`: PNL and positions metadata run sequentially.
104
+ * Both use `orderBy: id` to avoid timeouts on unindexed sort columns; the
105
+ * builder re-sorts by `amount desc` after fetching.
106
+ *
107
+ * Pair with `buildPolymarketActivity`.
108
+ */
109
+ const POLYMARKET_DEFAULT_SUBSCRIPTION = async (address, types, goldSkyFetch, baseUrl) => {
110
+ if (!types.includes('trades') && !types.includes('positions'))
111
+ return null;
112
+ // Trades (maker + taker) and PNL all run in parallel.
113
+ const [makerData, takerData, pnlData] = await Promise.all([
114
+ types.includes('trades') ? goldSkyFetch(BUILD_POLYMARKET_TRADES_AS_MAKER_QUERY(address, baseUrl)) : null,
115
+ types.includes('trades') ? goldSkyFetch(BUILD_POLYMARKET_TRADES_AS_TAKER_QUERY(address, baseUrl)) : null,
116
+ types.includes('positions') ? goldSkyFetch(BUILD_POLYMARKET_PNL_QUERY(address, baseUrl)) : null,
117
+ ]);
118
+ const result = {};
119
+ if (types.includes('trades')) {
120
+ const seen = new Set();
121
+ const trades = [];
122
+ for (const row of [...(makerData?.orderFilledEvents ?? []), ...(takerData?.orderFilledEvents ?? [])]) {
123
+ if (!seen.has(row.id)) {
124
+ seen.add(row.id);
125
+ trades.push(row);
126
+ }
127
+ }
128
+ trades.sort((a, b) => Number(b.timestamp) - Number(a.timestamp));
129
+ result.orderFilledEvents = trades;
130
+ }
131
+ if (pnlData) {
132
+ const sorted = (pnlData.userPositions ?? [])
133
+ .sort((a, b) => parseFloat(b.amount ?? '0') - parseFloat(a.amount ?? '0'));
134
+ result.userPositions = sorted;
135
+ const tokenIds = sorted.map((p) => String(p.tokenId));
136
+ if (tokenIds.length > 0) {
137
+ const metaData = await goldSkyFetch(BUILD_POLYMARKET_POSITIONS_QUERY(address, tokenIds, baseUrl));
138
+ if (metaData)
139
+ Object.assign(result, metaData);
140
+ }
141
+ }
142
+ return result;
143
+ };
144
+ exports.POLYMARKET_DEFAULT_SUBSCRIPTION = POLYMARKET_DEFAULT_SUBSCRIPTION;
145
+ /**
146
+ * Limitless: watches ERC-20 `Transfer` events on the USDC contract.
147
+ * Only active when `'balances'` is in the requested types.
148
+ *
149
+ * Pair with `buildLimitlessBalanceActivity`.
150
+ */
151
+ const LIMITLESS_DEFAULT_SUBSCRIPTION = async (address, types, fetch, baseUrl) => {
152
+ if (!types.includes('balances') || !baseUrl)
153
+ return null;
154
+ return fetch({
155
+ url: baseUrl,
156
+ query: /* GraphQL */ `
157
+ query WatchLimitlessAddress($address: Bytes!) {
158
+ transfers(
159
+ where: { or: [{ from: $address }, { to: $address }] }
160
+ first: 1
161
+ orderBy: blockTimestamp
162
+ orderDirection: desc
163
+ ) {
164
+ id
165
+ blockTimestamp
166
+ from
167
+ to
168
+ value
169
+ }
170
+ }
171
+ `,
172
+ variables: { address: address.toLowerCase() },
173
+ });
174
+ };
175
+ exports.LIMITLESS_DEFAULT_SUBSCRIPTION = LIMITLESS_DEFAULT_SUBSCRIPTION;
176
+ // ----------------------------------------------------------------------------
177
+ // Activity builders
178
+ // ----------------------------------------------------------------------------
179
+ /**
180
+ * Derives `Trade[]` from Polymarket CTF Exchange `OrderFilled` event data.
181
+ */
182
+ const buildPolymarketTradesActivity = (data, address, types) => {
183
+ if (!types.includes('trades'))
184
+ return null;
185
+ const filled = data?.orderFilledEvents;
186
+ if (!Array.isArray(filled) || filled.length === 0)
187
+ return null;
188
+ const addr = address.toLowerCase();
189
+ const trades = filled.map((f) => {
190
+ const isMaker = f.maker?.toLowerCase() === addr;
191
+ const currAssetId = BigInt(isMaker ? f.makerAssetId : f.takerAssetId);
192
+ const isBuying = currAssetId === 0n;
193
+ let shareAmount;
194
+ let usdcAmount;
195
+ if (isMaker) {
196
+ if (isBuying) {
197
+ usdcAmount = parseFloat(f.makerAmountFilled) / 1e6;
198
+ shareAmount = parseFloat(f.takerAmountFilled) / 1e6;
199
+ }
200
+ else {
201
+ shareAmount = parseFloat(f.makerAmountFilled) / 1e6;
202
+ usdcAmount = parseFloat(f.takerAmountFilled) / 1e6;
203
+ }
204
+ }
205
+ else {
206
+ if (isBuying) {
207
+ usdcAmount = parseFloat(f.takerAmountFilled) / 1e6;
208
+ shareAmount = parseFloat(f.makerAmountFilled) / 1e6;
209
+ }
210
+ else {
211
+ shareAmount = parseFloat(f.takerAmountFilled) / 1e6;
212
+ usdcAmount = parseFloat(f.makerAmountFilled) / 1e6;
213
+ }
214
+ }
215
+ return {
216
+ id: f.id,
217
+ timestamp: Number(f.timestamp) * 1000,
218
+ price: shareAmount > 0 ? usdcAmount / shareAmount : 0,
219
+ amount: shareAmount,
220
+ side: isBuying ? 'buy' : 'sell',
221
+ outcomeId: isMaker ? f.makerAssetId : f.takerAssetId,
222
+ };
223
+ });
224
+ return { trades };
225
+ };
226
+ exports.buildPolymarketTradesActivity = buildPolymarketTradesActivity;
227
+ /**
228
+ * Derives `Position[]` from the joined PNL + positions-metadata event data.
229
+ *
230
+ * `userPositions` (PNL) and `userBalances` (positions) are guaranteed
231
+ * to share the same tokenIds since step 2 is filtered by step 1's results.
232
+ *
233
+ * `currentPrice` and `unrealizedPnL` are left at 0 (not available on-chain).
234
+ */
235
+ /**
236
+ * Derives `Position[]` from joined PNL (`userPositions`) + metadata (`userBalances`).
237
+ * `currentPrice` and `unrealizedPnL` are left at 0 (not available on-chain).
238
+ */
239
+ const buildPolymarketPositionsActivity = (data, _address, types) => {
240
+ if (!types.includes('positions'))
241
+ return null;
242
+ const pnlRows = data?.userPositions ?? [];
243
+ if (pnlRows.length === 0)
244
+ return null;
245
+ const conditionRows = data?.tokenIdConditions ?? [];
246
+ const metaByToken = new Map();
247
+ for (const c of conditionRows) {
248
+ metaByToken.set(c.id ?? '', {
249
+ marketId: c.condition?.id ?? '',
250
+ outcomeIndex: Number(c.outcomeIndex ?? 0),
251
+ });
252
+ }
253
+ const positions = pnlRows.map((p) => {
254
+ const tokenId = String(p.tokenId ?? '');
255
+ const meta = metaByToken.get(tokenId);
256
+ return {
257
+ marketId: meta?.marketId ?? '',
258
+ outcomeId: tokenId,
259
+ outcomeLabel: (meta?.outcomeIndex ?? 0) === 1 ? 'Yes' : 'No',
260
+ size: parseFloat(p.amount ?? '0') / 1e6,
261
+ entryPrice: parseFloat(p.avgPrice ?? '0') / 1e6,
262
+ currentPrice: 0, // Not available on-chain
263
+ unrealizedPnL: 0, // Not available on-chain
264
+ realizedPnL: parseFloat(p.realizedPnl ?? '0') / 1e6,
265
+ };
266
+ });
267
+ return { positions };
268
+ };
269
+ exports.buildPolymarketPositionsActivity = buildPolymarketPositionsActivity;
270
+ /**
271
+ * Combined activity builder for Polymarket. Pair with `POLYMARKET_DEFAULT_SUBSCRIPTION`.
272
+ */
273
+ const buildPolymarketActivity = (data, address, types, lastSnapshot) => {
274
+ const result = {};
275
+ if (types.includes('trades')) {
276
+ const r = (0, exports.buildPolymarketTradesActivity)(data, address, types, lastSnapshot);
277
+ if (r?.trades)
278
+ result.trades = r.trades;
279
+ }
280
+ if (types.includes('positions')) {
281
+ const r = (0, exports.buildPolymarketPositionsActivity)(data, address, types, lastSnapshot);
282
+ if (r?.positions)
283
+ result.positions = r.positions;
284
+ }
285
+ return Object.keys(result).length > 0 ? result : null;
286
+ };
287
+ exports.buildPolymarketActivity = buildPolymarketActivity;
288
+ /**
289
+ * Derives a USDC balance delta from a Limitless ERC-20 transfer event.
290
+ * Returns `null` to fall back to full RPC fetch when baseline is missing.
291
+ */
292
+ const buildLimitlessBalanceActivity = (data, address, types, lastActivity) => {
293
+ if (!types.includes('balances'))
294
+ return null;
295
+ const transfers = data?.transfers;
296
+ if (!Array.isArray(transfers) || transfers.length === 0)
297
+ return null;
298
+ const prev = lastActivity?.balances?.find(b => b.currency === 'USDC');
299
+ if (!prev)
300
+ return null;
301
+ const t = transfers[0];
302
+ const addr = address.toLowerCase();
303
+ const isIncoming = t.to?.toLowerCase() === addr;
304
+ const delta = parseFloat(t.value) / 1e6;
305
+ const newTotal = Math.max(0, isIncoming ? prev.total + delta : prev.total - delta);
306
+ return {
307
+ balances: [{
308
+ currency: 'USDC',
309
+ total: newTotal,
310
+ available: Math.max(0, newTotal - prev.locked),
311
+ locked: prev.locked,
312
+ }],
313
+ };
314
+ };
315
+ exports.buildLimitlessBalanceActivity = buildLimitlessBalanceActivity;
316
+ // ----------------------------------------------------------------------------
317
+ // GoldSkySubscriber
318
+ // ----------------------------------------------------------------------------
319
+ /**
320
+ * Polls goldsky subgraph endpoints on a configurable interval.
321
+ *
322
+ * Passes a `GoldSkyFetch` helper to each builder invocation, allowing builders
323
+ * to chain requests sequentially or run them in parallel as needed.
324
+ */
325
+ class GoldSkySubscriber {
326
+ config;
327
+ pollMs;
328
+ abortControllers = new Map();
329
+ pollTimers = new Map();
330
+ callbacks = new Map();
331
+ addressQueryTypes = new Map();
332
+ closed = false;
333
+ constructor(config) {
334
+ this.config = config;
335
+ this.pollMs = config.pollMs ?? 3000;
336
+ }
337
+ async subscribe(address, types, onEvent) {
338
+ if (this.closed)
339
+ return;
340
+ this.callbacks.set(address, onEvent);
341
+ this.addressQueryTypes.set(address, types);
342
+ const existing = this.pollTimers.get(address);
343
+ if (existing) {
344
+ clearInterval(existing);
345
+ this.pollTimers.delete(address);
346
+ }
347
+ const timer = setInterval(() => this.query(address), this.pollMs);
348
+ this.pollTimers.set(address, timer);
349
+ }
350
+ unsubscribe(address) {
351
+ const timer = this.pollTimers.get(address);
352
+ if (timer) {
353
+ clearInterval(timer);
354
+ this.pollTimers.delete(address);
355
+ }
356
+ this.abortControllers.get(address)?.abort();
357
+ this.abortControllers.delete(address);
358
+ this.callbacks.delete(address);
359
+ this.addressQueryTypes.delete(address);
360
+ }
361
+ close() {
362
+ this.closed = true;
363
+ for (const address of [...this.pollTimers.keys()]) {
364
+ this.unsubscribe(address);
365
+ }
366
+ }
367
+ async query(address) {
368
+ const callback = this.callbacks.get(address);
369
+ const types = this.addressQueryTypes.get(address);
370
+ if (!callback || !types)
371
+ return;
372
+ this.abortControllers.get(address)?.abort();
373
+ const controller = new AbortController();
374
+ this.abortControllers.set(address, controller);
375
+ const goldSkyFetch = (q) => this.runQuery(q, controller.signal);
376
+ const data = await this.config.buildSubscription(address, types, goldSkyFetch, this.config.baseUrl);
377
+ if (!data)
378
+ return;
379
+ callback(data);
380
+ }
381
+ async runQuery(q, signal) {
382
+ const headers = { 'Content-Type': 'application/json' };
383
+ if (this.config.apiKey) {
384
+ headers['Authorization'] = `Bearer ${this.config.apiKey}`;
385
+ }
386
+ try {
387
+ const res = await fetch(q.url, {
388
+ method: 'POST',
389
+ headers,
390
+ body: JSON.stringify({ query: q.query, variables: q.variables ?? {} }),
391
+ signal,
392
+ });
393
+ if (!res.ok) {
394
+ console.warn(`[GoldSkySubscriber] HTTP ${res.status} from ${q.url}`);
395
+ return null;
396
+ }
397
+ const json = await res.json();
398
+ if (json?.errors) {
399
+ console.warn(`[GoldSkySubscriber] GraphQL errors from ${q.url}:`, JSON.stringify(json.errors));
400
+ return null;
401
+ }
402
+ return json?.data ?? null;
403
+ }
404
+ catch (err) {
405
+ if (err?.name !== 'AbortError') {
406
+ console.warn(`[GoldSkySubscriber] Fetch failed for ${q.url}:`, err);
407
+ }
408
+ return null;
409
+ }
410
+ }
411
+ }
412
+ exports.GoldSkySubscriber = GoldSkySubscriber;