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.
- package/dist/BaseExchange.d.ts +69 -30
- package/dist/BaseExchange.js +124 -82
- package/dist/exchanges/baozi/index.d.ts +2 -0
- package/dist/exchanges/baozi/index.js +2 -0
- package/dist/exchanges/baozi/price.d.ts +3 -0
- package/dist/exchanges/baozi/price.js +16 -0
- package/dist/exchanges/baozi/price.test.d.ts +1 -0
- package/dist/exchanges/baozi/price.test.js +33 -0
- package/dist/exchanges/baozi/utils.js +5 -9
- package/dist/exchanges/kalshi/api.d.ts +1 -1
- package/dist/exchanges/kalshi/api.js +1 -1
- package/dist/exchanges/kalshi/fetchOHLCV.js +5 -4
- package/dist/exchanges/kalshi/fetchOrderBook.js +21 -21
- package/dist/exchanges/kalshi/fetchTrades.js +2 -1
- package/dist/exchanges/kalshi/index.d.ts +3 -1
- package/dist/exchanges/kalshi/index.js +19 -16
- package/dist/exchanges/kalshi/price.d.ts +3 -0
- package/dist/exchanges/kalshi/price.js +14 -0
- package/dist/exchanges/kalshi/price.test.d.ts +1 -0
- package/dist/exchanges/kalshi/price.test.js +24 -0
- package/dist/exchanges/kalshi/utils.js +5 -4
- package/dist/exchanges/limitless/api.d.ts +1 -1
- package/dist/exchanges/limitless/api.js +1 -1
- package/dist/exchanges/limitless/index.d.ts +58 -19
- package/dist/exchanges/limitless/index.js +169 -101
- package/dist/exchanges/limitless/websocket.d.ts +10 -3
- package/dist/exchanges/limitless/websocket.js +71 -52
- package/dist/exchanges/myriad/api.d.ts +1 -1
- package/dist/exchanges/myriad/api.js +1 -1
- package/dist/exchanges/myriad/index.d.ts +3 -1
- package/dist/exchanges/myriad/index.js +7 -4
- package/dist/exchanges/myriad/price.d.ts +1 -0
- package/dist/exchanges/myriad/price.js +7 -0
- package/dist/exchanges/myriad/price.test.d.ts +1 -0
- package/dist/exchanges/myriad/price.test.js +17 -0
- 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/polymarket/index.d.ts +28 -15
- package/dist/exchanges/polymarket/index.js +217 -137
- package/dist/exchanges/polymarket/websocket.d.ts +11 -4
- package/dist/exchanges/polymarket/websocket.js +58 -36
- package/dist/exchanges/probable/api.d.ts +1 -1
- package/dist/exchanges/probable/api.js +1 -1
- package/dist/exchanges/probable/index.d.ts +2 -0
- package/dist/exchanges/probable/index.js +2 -0
- package/dist/subscriber/base.d.ts +82 -0
- package/dist/subscriber/base.js +2 -0
- package/dist/subscriber/external/goldsky.d.ts +96 -0
- package/dist/subscriber/external/goldsky.js +412 -0
- package/dist/subscriber/watcher.d.ts +85 -0
- package/dist/subscriber/watcher.js +178 -0
- package/dist/types.d.ts +5 -0
- 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-
|
|
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,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;
|