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.
- package/dist/chunk-VR5WP5S4.js +303 -0
- package/dist/index.js +26 -20
- package/dist/src/bitquery-ws.js +6 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
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) =>
|
|
2241
|
-
subscriptionId: params.subscriptionId
|
|
2242
|
-
})
|
|
2248
|
+
async (_id, params) => bitqueryStreamManager.unsubscribe(params.subscriptionId)
|
|
2243
2249
|
)
|
|
2244
2250
|
});
|
|
2245
2251
|
api.registerTool({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-traderclaw",
|
|
3
|
-
"version": "1.0.
|
|
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",
|