solana-traderclaw 1.0.90 → 1.0.91

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.
@@ -0,0 +1,303 @@
1
+ // src/bitquery-ws.ts
2
+ var RECONNECT_DELAYS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
3
+ var BitqueryStreamManager = class {
4
+ config;
5
+ ws = null;
6
+ authenticated = false;
7
+ connecting = false;
8
+ reconnectAttempt = 0;
9
+ reconnectTimer = null;
10
+ intentionalClose = false;
11
+ currentAccessToken = "";
12
+ // FIFO queue — server doesn't echo a requestId, so we match by arrival order
13
+ pendingSubscribeQueue = [];
14
+ // keyed by subscriptionId
15
+ pendingUnsubscribes = /* @__PURE__ */ new Map();
16
+ // tracks active subscriptions for auto-resubscribe on reconnect
17
+ activeSubscriptions = /* @__PURE__ */ new Map();
18
+ constructor(config) {
19
+ this.config = config;
20
+ }
21
+ async subscribe(params) {
22
+ await this.ensureConnected();
23
+ return new Promise((resolve, reject) => {
24
+ const timeout = setTimeout(() => {
25
+ const idx = this.pendingSubscribeQueue.findIndex((p) => p.resolve === resolve);
26
+ if (idx !== -1) this.pendingSubscribeQueue.splice(idx, 1);
27
+ reject(new Error("bitquery_subscribe timed out after 15 seconds"));
28
+ }, 15e3);
29
+ this.pendingSubscribeQueue.push({
30
+ resolve,
31
+ reject,
32
+ timeout,
33
+ templateKey: params.templateKey,
34
+ variables: params.variables || {},
35
+ agentId: params.agentId,
36
+ subscriberType: params.subscriberType
37
+ });
38
+ const msg = {
39
+ type: "bitquery_subscribe",
40
+ templateKey: params.templateKey,
41
+ variables: params.variables || {},
42
+ walletId: this.config.walletId
43
+ };
44
+ if (params.agentId) {
45
+ msg.agentId = params.agentId;
46
+ msg.subscriberType = params.subscriberType || "agent";
47
+ } else if (params.subscriberType) {
48
+ msg.subscriberType = params.subscriberType;
49
+ }
50
+ try {
51
+ this.ws.send(JSON.stringify(msg));
52
+ } catch (err) {
53
+ clearTimeout(timeout);
54
+ const idx = this.pendingSubscribeQueue.findIndex((p) => p.resolve === resolve);
55
+ if (idx !== -1) this.pendingSubscribeQueue.splice(idx, 1);
56
+ reject(new Error(`Failed to send subscribe: ${err}`));
57
+ }
58
+ });
59
+ }
60
+ async unsubscribe(subscriptionId) {
61
+ this.activeSubscriptions.delete(subscriptionId);
62
+ if (!this.ws || this.ws.readyState !== 1) {
63
+ return { unsubscribed: true };
64
+ }
65
+ return new Promise((resolve) => {
66
+ const timeout = setTimeout(() => {
67
+ this.pendingUnsubscribes.delete(subscriptionId);
68
+ resolve({ unsubscribed: true });
69
+ }, 1e4);
70
+ this.pendingUnsubscribes.set(subscriptionId, { resolve, timeout });
71
+ try {
72
+ this.ws.send(JSON.stringify({ type: "bitquery_unsubscribe", subscriptionId }));
73
+ } catch {
74
+ clearTimeout(timeout);
75
+ this.pendingUnsubscribes.delete(subscriptionId);
76
+ resolve({ unsubscribed: true });
77
+ }
78
+ });
79
+ }
80
+ /** Close the WS if no active subscriptions remain. */
81
+ disconnectIfIdle() {
82
+ if (this.activeSubscriptions.size === 0) {
83
+ this.close();
84
+ }
85
+ }
86
+ close() {
87
+ this.intentionalClose = true;
88
+ if (this.reconnectTimer) {
89
+ clearTimeout(this.reconnectTimer);
90
+ this.reconnectTimer = null;
91
+ }
92
+ if (this.ws) {
93
+ try {
94
+ this.ws.close();
95
+ } catch {
96
+ }
97
+ this.ws = null;
98
+ }
99
+ this.authenticated = false;
100
+ }
101
+ async ensureConnected() {
102
+ if (this.ws && this.ws.readyState === 1 && this.authenticated) return;
103
+ if (this.connecting) {
104
+ await new Promise((resolve, reject) => {
105
+ const timeout = setTimeout(() => reject(new Error("Timed out waiting for connection")), 2e4);
106
+ const check = setInterval(() => {
107
+ if (this.authenticated) {
108
+ clearTimeout(timeout);
109
+ clearInterval(check);
110
+ resolve();
111
+ }
112
+ }, 100);
113
+ });
114
+ return;
115
+ }
116
+ this.intentionalClose = false;
117
+ this.connecting = true;
118
+ try {
119
+ await this.connect();
120
+ await new Promise((resolve, reject) => {
121
+ const timeout = setTimeout(() => reject(new Error("Authentication timed out")), 15e3);
122
+ const check = setInterval(() => {
123
+ if (this.authenticated) {
124
+ clearTimeout(timeout);
125
+ clearInterval(check);
126
+ resolve();
127
+ }
128
+ }, 100);
129
+ });
130
+ } finally {
131
+ this.connecting = false;
132
+ }
133
+ }
134
+ async connect() {
135
+ const WebSocket = (await import("ws")).default;
136
+ this.currentAccessToken = await this.config.getAccessToken();
137
+ const url = `${this.config.wsUrl}?accessToken=${encodeURIComponent(this.currentAccessToken)}`;
138
+ this.authenticated = false;
139
+ this.log("info", `Connecting to ${this.config.wsUrl}`);
140
+ return new Promise((resolve, reject) => {
141
+ let ws;
142
+ try {
143
+ ws = new WebSocket(url);
144
+ this.ws = ws;
145
+ } catch (err) {
146
+ reject(err);
147
+ return;
148
+ }
149
+ const connectTimeout = setTimeout(() => {
150
+ if (ws.readyState !== 1) {
151
+ ws.close();
152
+ reject(new Error("WS connection timed out"));
153
+ }
154
+ }, 1e4);
155
+ ws.on("open", () => {
156
+ clearTimeout(connectTimeout);
157
+ this.reconnectAttempt = 0;
158
+ this.log("info", "Connected");
159
+ resolve();
160
+ });
161
+ ws.on("message", (data) => {
162
+ try {
163
+ const msg = JSON.parse(data.toString());
164
+ this.handleMessage(msg);
165
+ } catch {
166
+ this.log("warn", "Failed to parse message");
167
+ }
168
+ });
169
+ ws.on("close", () => {
170
+ clearTimeout(connectTimeout);
171
+ this.authenticated = false;
172
+ this.log("info", "WS closed");
173
+ this.drainPendingOnClose();
174
+ if (!this.intentionalClose && this.activeSubscriptions.size > 0) {
175
+ this.scheduleReconnect();
176
+ }
177
+ });
178
+ ws.on("error", (err) => {
179
+ clearTimeout(connectTimeout);
180
+ this.log("error", `WS error: ${err.message}`);
181
+ if (ws.readyState !== 1) {
182
+ reject(err);
183
+ }
184
+ });
185
+ });
186
+ }
187
+ handleMessage(msg) {
188
+ switch (msg.type) {
189
+ case "connected":
190
+ this.log("info", "Handshake received, authenticating...");
191
+ if (this.ws && this.ws.readyState === 1) {
192
+ this.ws.send(JSON.stringify({ type: "auth", accessToken: this.currentAccessToken }));
193
+ }
194
+ break;
195
+ case "authenticated":
196
+ this.authenticated = true;
197
+ this.log("info", "Authenticated");
198
+ void this.resubscribeAll();
199
+ break;
200
+ case "bitquery_subscribed": {
201
+ const subscriptionId = msg.subscriptionId;
202
+ const streamKey = msg.streamKey;
203
+ const pending = this.pendingSubscribeQueue.shift();
204
+ if (pending) {
205
+ clearTimeout(pending.timeout);
206
+ this.activeSubscriptions.set(subscriptionId, {
207
+ subscriptionId,
208
+ templateKey: pending.templateKey,
209
+ variables: pending.variables,
210
+ agentId: pending.agentId,
211
+ subscriberType: pending.subscriberType
212
+ });
213
+ pending.resolve({ subscriptionId, streamKey });
214
+ }
215
+ break;
216
+ }
217
+ case "bitquery_unsubscribed": {
218
+ const subscriptionId = msg.subscriptionId;
219
+ this.activeSubscriptions.delete(subscriptionId);
220
+ const pending = this.pendingUnsubscribes.get(subscriptionId);
221
+ if (pending) {
222
+ clearTimeout(pending.timeout);
223
+ this.pendingUnsubscribes.delete(subscriptionId);
224
+ pending.resolve({ unsubscribed: true });
225
+ }
226
+ this.disconnectIfIdle();
227
+ break;
228
+ }
229
+ case "error": {
230
+ const code = msg.code;
231
+ this.log("error", `${code}: ${msg.message || ""}`);
232
+ if ([
233
+ "WS_SUBSCRIBE_VALIDATION_ERROR",
234
+ "BITQUERY_SUBSCRIPTION_TEMPLATE_NOT_FOUND",
235
+ "WS_SUBSCRIPTION_LIMIT_REACHED",
236
+ "WS_BRIDGE_UNAVAILABLE"
237
+ ].includes(code)) {
238
+ const pending = this.pendingSubscribeQueue.shift();
239
+ if (pending) {
240
+ clearTimeout(pending.timeout);
241
+ pending.reject(new Error(`${code}: ${msg.message || ""}`));
242
+ }
243
+ }
244
+ if (["WS_AUTH_REQUIRED", "WS_AUTH_INVALID", "ACCESS_TOKEN_EXPIRED"].includes(code)) {
245
+ this.close();
246
+ }
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ drainPendingOnClose() {
252
+ for (const pending of this.pendingSubscribeQueue) {
253
+ clearTimeout(pending.timeout);
254
+ pending.reject(new Error("WebSocket closed before subscription was confirmed"));
255
+ }
256
+ this.pendingSubscribeQueue = [];
257
+ for (const [, pending] of this.pendingUnsubscribes) {
258
+ clearTimeout(pending.timeout);
259
+ pending.resolve({ unsubscribed: true });
260
+ }
261
+ this.pendingUnsubscribes.clear();
262
+ }
263
+ async resubscribeAll() {
264
+ if (this.activeSubscriptions.size === 0) return;
265
+ const subs = [...this.activeSubscriptions.values()];
266
+ this.activeSubscriptions.clear();
267
+ this.log("info", `Re-subscribing ${subs.length} subscription(s) after reconnect`);
268
+ for (const sub of subs) {
269
+ try {
270
+ const result = await this.subscribe({
271
+ templateKey: sub.templateKey,
272
+ variables: sub.variables,
273
+ agentId: sub.agentId,
274
+ subscriberType: sub.subscriberType
275
+ });
276
+ this.log("info", `Re-subscribed ${sub.templateKey} \u2192 new id: ${result.subscriptionId}`);
277
+ } catch (err) {
278
+ this.log("error", `Re-subscribe failed for ${sub.templateKey}: ${err}`);
279
+ }
280
+ }
281
+ }
282
+ scheduleReconnect() {
283
+ if (this.intentionalClose) return;
284
+ const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
285
+ this.reconnectAttempt++;
286
+ this.log("info", `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
287
+ this.reconnectTimer = setTimeout(async () => {
288
+ try {
289
+ await this.connect();
290
+ } catch (err) {
291
+ this.log("error", `Reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
292
+ this.scheduleReconnect();
293
+ }
294
+ }, delay);
295
+ }
296
+ log(level, msg) {
297
+ this.config.logger?.[level](`[bitquery-ws] ${msg}`);
298
+ }
299
+ };
300
+
301
+ export {
302
+ BitqueryStreamManager
303
+ };
package/dist/index.js CHANGED
@@ -16,18 +16,15 @@ import {
16
16
  import {
17
17
  AlphaStreamManager
18
18
  } from "./chunk-3YPZOXWE.js";
19
+ import {
20
+ BitqueryStreamManager
21
+ } from "./chunk-VR5WP5S4.js";
19
22
  import {
20
23
  orchestratorRequest
21
24
  } from "./chunk-NDPVVAV7.js";
22
25
  import {
23
26
  IntelligenceLab
24
27
  } from "./chunk-FBS5FGW2.js";
25
- import {
26
- scrubUntrustedText
27
- } from "./chunk-AI6MTHUN.js";
28
- import {
29
- readRecoverySecretFromDisk
30
- } from "./chunk-SBYHSJLU.js";
31
28
  import {
32
29
  generateBulletinDigest,
33
30
  generateDecisionDigest,
@@ -35,6 +32,12 @@ import {
35
32
  resolveMemoryDir,
36
33
  resolveWorkspaceRoot
37
34
  } from "./chunk-JO3BXAUQ.js";
35
+ import {
36
+ scrubUntrustedText
37
+ } from "./chunk-AI6MTHUN.js";
38
+ import {
39
+ readRecoverySecretFromDisk
40
+ } from "./chunk-SBYHSJLU.js";
38
41
 
39
42
  // index.ts
40
43
  import { Type } from "@sinclair/typebox";
@@ -2205,6 +2208,16 @@ ${notes}
2205
2208
  })
2206
2209
  )
2207
2210
  });
2211
+ const bitqueryStreamManager = new BitqueryStreamManager({
2212
+ wsUrl: orchestratorUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/ws",
2213
+ walletId,
2214
+ getAccessToken: () => sessionManager.getAccessToken(),
2215
+ logger: {
2216
+ info: (msg) => api.logger.info(`[solana-trader] ${msg}`),
2217
+ warn: (msg) => api.logger.warn(`[solana-trader] ${msg}`),
2218
+ error: (msg) => api.logger.error(`[solana-trader] ${msg}`)
2219
+ }
2220
+ });
2208
2221
  api.registerTool({
2209
2222
  name: "solana_bitquery_subscribe",
2210
2223
  description: "Subscribe to a managed real-time Bitquery data stream. The orchestrator manages the WebSocket connection and broadcasts events. Available templates: realtimeTokenPricesSolana, ohlc1s, dexPoolLiquidityChanges, pumpFunTokenCreation, pumpFunTrades, pumpSwapTrades, raydiumNewPools. Returns a subscriptionId for tracking. Pass agentId to enable event-to-agent forwarding \u2014 orchestrator delivers each event to your Gateway via /v1/responses in addition to normal WS delivery. Subscriptions expire after 24h and emit subscription_expiring/subscription_expired events. See websocket-streaming.md in the solana-trader skill for the full message contract and usage patterns.",
@@ -2215,18 +2228,13 @@ ${notes}
2215
2228
  subscriberType: Type.Optional(Type.Union([Type.Literal("agent"), Type.Literal("client")], { description: "Subscriber type. Inferred as 'agent' when agentId is present. Defaults to 'client'." }))
2216
2229
  }),
2217
2230
  execute: wrapExecute("solana_bitquery_subscribe", async (_id, params) => {
2218
- const body = {
2219
- templateKey: params.templateKey,
2220
- variables: params.variables || {}
2221
- };
2222
2231
  const effectiveAgentId = params.agentId || config.agentId;
2223
- if (effectiveAgentId) {
2224
- body.agentId = effectiveAgentId;
2225
- body.subscriberType = params.subscriberType || "agent";
2226
- } else if (params.subscriberType) {
2227
- body.subscriberType = params.subscriberType;
2228
- }
2229
- return post("/api/bitquery/subscribe", body);
2232
+ return bitqueryStreamManager.subscribe({
2233
+ templateKey: params.templateKey,
2234
+ variables: params.variables || {},
2235
+ agentId: effectiveAgentId,
2236
+ subscriberType: params.subscriberType || (effectiveAgentId ? "agent" : void 0)
2237
+ });
2230
2238
  })
2231
2239
  });
2232
2240
  api.registerTool({
@@ -2237,9 +2245,7 @@ ${notes}
2237
2245
  }),
2238
2246
  execute: wrapExecute(
2239
2247
  "solana_bitquery_unsubscribe",
2240
- async (_id, params) => post("/api/bitquery/unsubscribe", {
2241
- subscriptionId: params.subscriptionId
2242
- })
2248
+ async (_id, params) => bitqueryStreamManager.unsubscribe(params.subscriptionId)
2243
2249
  )
2244
2250
  });
2245
2251
  api.registerTool({
@@ -0,0 +1,6 @@
1
+ import {
2
+ BitqueryStreamManager
3
+ } from "../chunk-VR5WP5S4.js";
4
+ export {
5
+ BitqueryStreamManager
6
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solana-traderclaw",
3
- "version": "1.0.90",
3
+ "version": "1.0.91",
4
4
  "description": "TraderClaw V1-Upgraded — Solana trading for OpenClaw with intelligence lab, tool envelopes, prompt scrubbing, read-only X social intel, and split skill docs",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",