solana-traderclaw 1.0.89 → 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 +28 -23
- 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,26 +16,28 @@ 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,
|
|
34
31
|
generateEntitlementsDigest,
|
|
35
|
-
generateStateMd,
|
|
36
32
|
resolveMemoryDir,
|
|
37
33
|
resolveWorkspaceRoot
|
|
38
34
|
} from "./chunk-JO3BXAUQ.js";
|
|
35
|
+
import {
|
|
36
|
+
scrubUntrustedText
|
|
37
|
+
} from "./chunk-AI6MTHUN.js";
|
|
38
|
+
import {
|
|
39
|
+
readRecoverySecretFromDisk
|
|
40
|
+
} from "./chunk-SBYHSJLU.js";
|
|
39
41
|
|
|
40
42
|
// index.ts
|
|
41
43
|
import { Type } from "@sinclair/typebox";
|
|
@@ -2206,6 +2208,16 @@ ${notes}
|
|
|
2206
2208
|
})
|
|
2207
2209
|
)
|
|
2208
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
|
+
});
|
|
2209
2221
|
api.registerTool({
|
|
2210
2222
|
name: "solana_bitquery_subscribe",
|
|
2211
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.",
|
|
@@ -2216,18 +2228,13 @@ ${notes}
|
|
|
2216
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'." }))
|
|
2217
2229
|
}),
|
|
2218
2230
|
execute: wrapExecute("solana_bitquery_subscribe", async (_id, params) => {
|
|
2219
|
-
const body = {
|
|
2220
|
-
templateKey: params.templateKey,
|
|
2221
|
-
variables: params.variables || {}
|
|
2222
|
-
};
|
|
2223
2231
|
const effectiveAgentId = params.agentId || config.agentId;
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
}
|
|
2230
|
-
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
|
+
});
|
|
2231
2238
|
})
|
|
2232
2239
|
});
|
|
2233
2240
|
api.registerTool({
|
|
@@ -2238,9 +2245,7 @@ ${notes}
|
|
|
2238
2245
|
}),
|
|
2239
2246
|
execute: wrapExecute(
|
|
2240
2247
|
"solana_bitquery_unsubscribe",
|
|
2241
|
-
async (_id, params) =>
|
|
2242
|
-
subscriptionId: params.subscriptionId
|
|
2243
|
-
})
|
|
2248
|
+
async (_id, params) => bitqueryStreamManager.unsubscribe(params.subscriptionId)
|
|
2244
2249
|
)
|
|
2245
2250
|
});
|
|
2246
2251
|
api.registerTool({
|
|
@@ -3905,7 +3910,7 @@ ${String(params.summary)}
|
|
|
3905
3910
|
const stateFile = path.join(stateDir, `${bootAgentId}.json`);
|
|
3906
3911
|
const stateData = readJsonFile(stateFile);
|
|
3907
3912
|
if (stateData) {
|
|
3908
|
-
const stateMd =
|
|
3913
|
+
const stateMd = generateMemoryMd(bootAgentId, stateData.state || null);
|
|
3909
3914
|
context.bootstrapFiles.push({
|
|
3910
3915
|
name: `${bootAgentId}-state.md`,
|
|
3911
3916
|
path: `state/${bootAgentId}-state.md`,
|
|
@@ -4124,7 +4129,7 @@ Context compaction triggered. STATE.md synced from last persisted state. Decisio
|
|
|
4124
4129
|
const stateFile = path.join(stateDir, `${assembleAgentId}.json`);
|
|
4125
4130
|
const stateData = readJsonFile(stateFile);
|
|
4126
4131
|
if (stateData?.state) {
|
|
4127
|
-
lines.push(
|
|
4132
|
+
lines.push(generateMemoryMd(assembleAgentId, stateData.state));
|
|
4128
4133
|
}
|
|
4129
4134
|
} catch {
|
|
4130
4135
|
}
|
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",
|