@chainalert/cli 0.0.1
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/alerts-STXD4TE5.js +134 -0
- package/dist/auth-6LTJFNU2.js +124 -0
- package/dist/channels-C5JZON37.js +179 -0
- package/dist/chunk-6FDEYAAT.js +27 -0
- package/dist/chunk-CE4DKHAY.js +28 -0
- package/dist/chunk-IC5RERFB.js +185 -0
- package/dist/chunk-K2BGDX7X.js +38 -0
- package/dist/chunk-WPW7UBVR.js +95 -0
- package/dist/contracts-J7JYPQXP.js +108 -0
- package/dist/detections-QSMNEXXI.js +282 -0
- package/dist/events-FM7VK5VH.js +104 -0
- package/dist/health-7R5HLRCZ.js +40 -0
- package/dist/index.js +159 -0
- package/dist/networks-CF2ARYQO.js +57 -0
- package/dist/org-contracts-DM2BTHTO.js +175 -0
- package/dist/repl-Q43IBAFT.js +1657 -0
- package/dist/rpc-configs-P2QCFCDJ.js +112 -0
- package/dist/state-changes-B6BIS4OJ.js +67 -0
- package/dist/templates-LYY2SV42.js +106 -0
- package/package.json +36 -0
|
@@ -0,0 +1,1657 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
confirm
|
|
4
|
+
} from "./chunk-6FDEYAAT.js";
|
|
5
|
+
import {
|
|
6
|
+
addRpcConfig,
|
|
7
|
+
createChannel,
|
|
8
|
+
createDetection,
|
|
9
|
+
deleteChannel,
|
|
10
|
+
deleteDetection,
|
|
11
|
+
getAlert,
|
|
12
|
+
getAlertStats,
|
|
13
|
+
getDetection,
|
|
14
|
+
getEvent,
|
|
15
|
+
getStorageSlots,
|
|
16
|
+
getTemplate,
|
|
17
|
+
listAlerts,
|
|
18
|
+
listChannels,
|
|
19
|
+
listDetections,
|
|
20
|
+
listEvents,
|
|
21
|
+
listNetworks,
|
|
22
|
+
listOrgContracts,
|
|
23
|
+
listRpcConfigs,
|
|
24
|
+
listStateChanges,
|
|
25
|
+
listTemplates,
|
|
26
|
+
registerOrgContract,
|
|
27
|
+
resolveContract,
|
|
28
|
+
testChannel,
|
|
29
|
+
testDetection,
|
|
30
|
+
updateDetection
|
|
31
|
+
} from "./chunk-IC5RERFB.js";
|
|
32
|
+
import {
|
|
33
|
+
getApiKey,
|
|
34
|
+
getApiUrl,
|
|
35
|
+
loadConfig
|
|
36
|
+
} from "./chunk-K2BGDX7X.js";
|
|
37
|
+
import {
|
|
38
|
+
bold,
|
|
39
|
+
cyan,
|
|
40
|
+
dim,
|
|
41
|
+
print,
|
|
42
|
+
printDim,
|
|
43
|
+
printError,
|
|
44
|
+
printWarn
|
|
45
|
+
} from "./chunk-WPW7UBVR.js";
|
|
46
|
+
|
|
47
|
+
// src/repl/repl.ts
|
|
48
|
+
import { createInterface } from "readline";
|
|
49
|
+
|
|
50
|
+
// src/ai/client.ts
|
|
51
|
+
import OpenAI from "openai";
|
|
52
|
+
function createOpenAIClient() {
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
if (config.openaiKey) {
|
|
55
|
+
return new OpenAI({ apiKey: config.openaiKey });
|
|
56
|
+
}
|
|
57
|
+
if (config.apiKey) {
|
|
58
|
+
return new OpenAI({
|
|
59
|
+
apiKey: config.apiKey,
|
|
60
|
+
baseURL: `${getApiUrl()}/ai/v1`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
throw new Error(
|
|
64
|
+
"No OpenAI key configured. Run: chainalert config set openai-key <your-key>"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/ai/registry.ts
|
|
69
|
+
var registry = [
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Networks
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
{
|
|
74
|
+
name: "list_networks",
|
|
75
|
+
description: "List all blockchain networks supported by ChainAlert. Returns network names, slugs, chain IDs, and status. Use this when the user asks about supported chains or networks.",
|
|
76
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
77
|
+
execute: () => listNetworks()
|
|
78
|
+
},
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Templates
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
{
|
|
83
|
+
name: "list_templates",
|
|
84
|
+
description: "List available detection templates. Templates are pre-built monitoring configurations that users can activate. Optionally filter by category or search term.",
|
|
85
|
+
parameters: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {
|
|
88
|
+
category: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: 'Filter by template category, e.g. "token-activity", "balance", "governance", "custom".'
|
|
91
|
+
},
|
|
92
|
+
search: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Free-text search across template names and descriptions."
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
required: []
|
|
98
|
+
},
|
|
99
|
+
execute: (args) => listTemplates(args)
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "get_template",
|
|
103
|
+
description: "Get full details of a specific detection template by its slug. Returns the template definition including all required/optional inputs, rule configuration, and metadata.",
|
|
104
|
+
parameters: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
slug: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: 'The template slug, e.g. "large-transfer", "fund-drainage", "ownership-transfer".'
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
required: ["slug"]
|
|
113
|
+
},
|
|
114
|
+
execute: (args) => getTemplate(args.slug)
|
|
115
|
+
},
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Detections
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
{
|
|
120
|
+
name: "list_detections",
|
|
121
|
+
description: "List the user's detections (active monitoring configurations). Detections are created from templates and watch specific contracts on specific networks. Optionally filter by status or network.",
|
|
122
|
+
parameters: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
status: {
|
|
126
|
+
type: "string",
|
|
127
|
+
description: 'Filter by status: "active", "paused", or "error".'
|
|
128
|
+
},
|
|
129
|
+
networkId: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: 'Filter by network ID, e.g. "ethereum-mainnet", "arbitrum-one".'
|
|
132
|
+
},
|
|
133
|
+
page: { type: "number", description: "Page number (1-based)." },
|
|
134
|
+
limit: { type: "number", description: "Results per page (default 20, max 100)." }
|
|
135
|
+
},
|
|
136
|
+
required: []
|
|
137
|
+
},
|
|
138
|
+
execute: (args) => listDetections(
|
|
139
|
+
args
|
|
140
|
+
)
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "get_detection",
|
|
144
|
+
description: "Get full details of a specific detection by ID, including its configuration, rules, status, associated channels, and recent activity.",
|
|
145
|
+
parameters: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
id: { type: "string", description: "The detection ID." }
|
|
149
|
+
},
|
|
150
|
+
required: ["id"]
|
|
151
|
+
},
|
|
152
|
+
execute: (args) => getDetection(args.id)
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "create_detection",
|
|
156
|
+
description: "Create a new detection from a template. This activates blockchain monitoring for the specified contract and network. Requires the template slug, a name, a network ID, and the template-specific inputs. The user should confirm before this runs.",
|
|
157
|
+
parameters: {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: {
|
|
160
|
+
templateSlug: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: 'The slug of the template to use, e.g. "large-transfer", "fund-drainage".'
|
|
163
|
+
},
|
|
164
|
+
name: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "A human-readable name for this detection."
|
|
167
|
+
},
|
|
168
|
+
networkId: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: 'The network ID to monitor, e.g. "ethereum-mainnet", "arbitrum-one", "base-mainnet", "polygon-mainnet".'
|
|
171
|
+
},
|
|
172
|
+
inputs: {
|
|
173
|
+
type: "object",
|
|
174
|
+
description: "Template-specific input values. Keys must match the template's input definitions."
|
|
175
|
+
},
|
|
176
|
+
contractAddress: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description: "The contract address to monitor (0x-prefixed hex)."
|
|
179
|
+
},
|
|
180
|
+
channelIds: {
|
|
181
|
+
type: "array",
|
|
182
|
+
items: { type: "string" },
|
|
183
|
+
description: "IDs of notification channels to attach."
|
|
184
|
+
},
|
|
185
|
+
severity: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: 'Override severity: "info", "low", "medium", "high", or "critical".'
|
|
188
|
+
},
|
|
189
|
+
cooldownMinutes: {
|
|
190
|
+
type: "number",
|
|
191
|
+
description: "Minimum minutes between repeated alerts for the same detection (default varies by template)."
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
required: ["templateSlug", "name", "networkId", "inputs"]
|
|
195
|
+
},
|
|
196
|
+
execute: (args) => createDetection(
|
|
197
|
+
args
|
|
198
|
+
),
|
|
199
|
+
confirm: (args) => `Creating detection "${args.name}" using template "${args.templateSlug}" on network "${args.networkId}"`
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "update_detection",
|
|
203
|
+
description: "Update an existing detection. Can change its name, status (pause/resume), inputs, notification channels, or cooldown. The user should confirm before this runs.",
|
|
204
|
+
parameters: {
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {
|
|
207
|
+
id: { type: "string", description: "The detection ID to update." },
|
|
208
|
+
name: { type: "string", description: "New name for the detection." },
|
|
209
|
+
status: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: 'New status: "active" or "paused".'
|
|
212
|
+
},
|
|
213
|
+
inputs: {
|
|
214
|
+
type: "object",
|
|
215
|
+
description: "Updated template input values."
|
|
216
|
+
},
|
|
217
|
+
channelIds: {
|
|
218
|
+
type: "array",
|
|
219
|
+
items: { type: "string" },
|
|
220
|
+
description: "Updated list of notification channel IDs."
|
|
221
|
+
},
|
|
222
|
+
cooldownMinutes: {
|
|
223
|
+
type: "number",
|
|
224
|
+
description: "New cooldown in minutes between repeated alerts."
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
required: ["id"]
|
|
228
|
+
},
|
|
229
|
+
execute: (args) => {
|
|
230
|
+
const { id, ...body } = args;
|
|
231
|
+
return updateDetection(id, body);
|
|
232
|
+
},
|
|
233
|
+
confirm: (args) => `Updating detection ${args.id}${args.status ? ` (status -> ${args.status})` : ""}`
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "delete_detection",
|
|
237
|
+
description: "Permanently delete a detection. This stops all monitoring and removes the detection. The user should confirm before this runs.",
|
|
238
|
+
parameters: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
id: { type: "string", description: "The detection ID to delete." }
|
|
242
|
+
},
|
|
243
|
+
required: ["id"]
|
|
244
|
+
},
|
|
245
|
+
execute: (args) => deleteDetection(args.id),
|
|
246
|
+
confirm: (args) => `Deleting detection ${args.id}`
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "test_detection",
|
|
250
|
+
description: "Test a detection against a specific historical block. This runs the detection rules against the block's data and returns whether the detection would have fired. Useful for validating configurations.",
|
|
251
|
+
parameters: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: {
|
|
254
|
+
id: { type: "string", description: "The detection ID to test." },
|
|
255
|
+
blockNumber: {
|
|
256
|
+
type: "number",
|
|
257
|
+
description: "The block number to test against."
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
required: ["id", "blockNumber"]
|
|
261
|
+
},
|
|
262
|
+
execute: (args) => testDetection(args.id, { blockNumber: args.blockNumber }),
|
|
263
|
+
confirm: (args) => `Testing detection ${args.id} against block ${args.blockNumber}`
|
|
264
|
+
},
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Alerts
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
{
|
|
269
|
+
name: "list_alerts",
|
|
270
|
+
description: "List alerts that have been triggered by the user's detections. Alerts are the actual notifications generated when a detection rule fires. Optionally filter by detection, severity, or date range.",
|
|
271
|
+
parameters: {
|
|
272
|
+
type: "object",
|
|
273
|
+
properties: {
|
|
274
|
+
detectionId: {
|
|
275
|
+
type: "string",
|
|
276
|
+
description: "Filter alerts to a specific detection ID."
|
|
277
|
+
},
|
|
278
|
+
severity: {
|
|
279
|
+
type: "string",
|
|
280
|
+
description: 'Filter by severity: "info", "low", "medium", "high", or "critical".'
|
|
281
|
+
},
|
|
282
|
+
from: {
|
|
283
|
+
type: "string",
|
|
284
|
+
description: "Start date (ISO 8601 format, e.g. 2024-01-01)."
|
|
285
|
+
},
|
|
286
|
+
to: {
|
|
287
|
+
type: "string",
|
|
288
|
+
description: "End date (ISO 8601 format, e.g. 2024-12-31)."
|
|
289
|
+
},
|
|
290
|
+
page: { type: "number", description: "Page number (1-based)." },
|
|
291
|
+
limit: { type: "number", description: "Results per page (default 20, max 100)." }
|
|
292
|
+
},
|
|
293
|
+
required: []
|
|
294
|
+
},
|
|
295
|
+
execute: (args) => listAlerts(
|
|
296
|
+
args
|
|
297
|
+
)
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "get_alert",
|
|
301
|
+
description: "Get full details of a specific alert by ID, including the triggering event, detection context, and notification delivery status.",
|
|
302
|
+
parameters: {
|
|
303
|
+
type: "object",
|
|
304
|
+
properties: {
|
|
305
|
+
id: { type: "string", description: "The alert ID." }
|
|
306
|
+
},
|
|
307
|
+
required: ["id"]
|
|
308
|
+
},
|
|
309
|
+
execute: (args) => getAlert(args.id)
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: "get_alert_stats",
|
|
313
|
+
description: "Get aggregate alert statistics for the user's organization. Returns counts by severity, detection, and time period. Use this when the user asks for a summary or overview of their alerts.",
|
|
314
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
315
|
+
execute: () => getAlertStats()
|
|
316
|
+
},
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Events
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
{
|
|
321
|
+
name: "list_events",
|
|
322
|
+
description: "List blockchain events captured by the user's detections. Events are the raw on-chain occurrences (e.g. Transfer, Approval) that detections monitor. Optionally filter by detection, event name, transaction hash, or date range.",
|
|
323
|
+
parameters: {
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
detectionId: {
|
|
327
|
+
type: "string",
|
|
328
|
+
description: "Filter events to a specific detection ID."
|
|
329
|
+
},
|
|
330
|
+
eventName: {
|
|
331
|
+
type: "string",
|
|
332
|
+
description: 'Filter by event name, e.g. "Transfer", "Approval".'
|
|
333
|
+
},
|
|
334
|
+
txHash: {
|
|
335
|
+
type: "string",
|
|
336
|
+
description: "Filter by transaction hash (0x-prefixed)."
|
|
337
|
+
},
|
|
338
|
+
from: { type: "string", description: "Start date (ISO 8601)." },
|
|
339
|
+
to: { type: "string", description: "End date (ISO 8601)." },
|
|
340
|
+
page: { type: "number", description: "Page number (1-based)." },
|
|
341
|
+
limit: { type: "number", description: "Results per page (default 20, max 100)." }
|
|
342
|
+
},
|
|
343
|
+
required: []
|
|
344
|
+
},
|
|
345
|
+
execute: (args) => listEvents(
|
|
346
|
+
args
|
|
347
|
+
)
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "get_event",
|
|
351
|
+
description: "Get full details of a specific blockchain event by ID, including decoded parameters and the associated transaction.",
|
|
352
|
+
parameters: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
id: { type: "string", description: "The event ID." }
|
|
356
|
+
},
|
|
357
|
+
required: ["id"]
|
|
358
|
+
},
|
|
359
|
+
execute: (args) => getEvent(args.id)
|
|
360
|
+
},
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// State Changes
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
{
|
|
365
|
+
name: "list_state_changes",
|
|
366
|
+
description: "List storage state changes detected by state-polling detections (e.g. proxy upgrades, custom storage slot monitors). Shows previous and current values.",
|
|
367
|
+
parameters: {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties: {
|
|
370
|
+
detectionId: {
|
|
371
|
+
type: "string",
|
|
372
|
+
description: "Filter to a specific detection ID."
|
|
373
|
+
},
|
|
374
|
+
page: { type: "number", description: "Page number (1-based)." },
|
|
375
|
+
limit: { type: "number", description: "Results per page (default 20, max 100)." }
|
|
376
|
+
},
|
|
377
|
+
required: []
|
|
378
|
+
},
|
|
379
|
+
execute: (args) => listStateChanges(args)
|
|
380
|
+
},
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// Channels
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
{
|
|
385
|
+
name: "list_channels",
|
|
386
|
+
description: "List notification channels configured by the user. Channels are destinations where alerts are sent (e.g. Slack, Discord, email, webhook). Optionally filter by type.",
|
|
387
|
+
parameters: {
|
|
388
|
+
type: "object",
|
|
389
|
+
properties: {
|
|
390
|
+
type: {
|
|
391
|
+
type: "string",
|
|
392
|
+
description: 'Filter by channel type: "slack", "discord", "email", "webhook", "telegram", "pagerduty".'
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
required: []
|
|
396
|
+
},
|
|
397
|
+
execute: (args) => listChannels(args)
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "create_channel",
|
|
401
|
+
description: "Create a new notification channel. The config object varies by type: Slack needs webhookUrl, Discord needs webhookUrl, email needs address, webhook needs url and optional headers, Telegram needs chatId and botToken, PagerDuty needs routingKey. The user should confirm before this runs.",
|
|
402
|
+
parameters: {
|
|
403
|
+
type: "object",
|
|
404
|
+
properties: {
|
|
405
|
+
name: {
|
|
406
|
+
type: "string",
|
|
407
|
+
description: "A human-readable name for this channel."
|
|
408
|
+
},
|
|
409
|
+
type: {
|
|
410
|
+
type: "string",
|
|
411
|
+
description: 'Channel type: "slack", "discord", "email", "webhook", "telegram", "pagerduty".'
|
|
412
|
+
},
|
|
413
|
+
config: {
|
|
414
|
+
type: "object",
|
|
415
|
+
description: "Channel-specific configuration. For Slack/Discord: { webhookUrl }. For email: { address }. For webhook: { url, headers? }. For Telegram: { chatId, botToken }. For PagerDuty: { routingKey }."
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
required: ["name", "type", "config"]
|
|
419
|
+
},
|
|
420
|
+
execute: (args) => createChannel(
|
|
421
|
+
args
|
|
422
|
+
),
|
|
423
|
+
confirm: (args) => `Creating ${args.type} notification channel "${args.name}"`
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "delete_channel",
|
|
427
|
+
description: "Delete a notification channel. Any detections using this channel will stop receiving notifications through it. The user should confirm before this runs.",
|
|
428
|
+
parameters: {
|
|
429
|
+
type: "object",
|
|
430
|
+
properties: {
|
|
431
|
+
id: { type: "string", description: "The channel ID to delete." }
|
|
432
|
+
},
|
|
433
|
+
required: ["id"]
|
|
434
|
+
},
|
|
435
|
+
execute: (args) => deleteChannel(args.id),
|
|
436
|
+
confirm: (args) => `Deleting notification channel ${args.id}`
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "test_channel",
|
|
440
|
+
description: "Send a test notification to a channel to verify it is configured correctly. The user should confirm before this runs.",
|
|
441
|
+
parameters: {
|
|
442
|
+
type: "object",
|
|
443
|
+
properties: {
|
|
444
|
+
id: { type: "string", description: "The channel ID to test." }
|
|
445
|
+
},
|
|
446
|
+
required: ["id"]
|
|
447
|
+
},
|
|
448
|
+
execute: (args) => testChannel(args.id),
|
|
449
|
+
confirm: (args) => `Sending test notification to channel ${args.id}`
|
|
450
|
+
},
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// Contracts
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
{
|
|
455
|
+
name: "resolve_contract",
|
|
456
|
+
description: "Resolve a contract address on a specific network. Fetches the contract's ABI, name, and other metadata from the block explorer. Use this before creating detections to verify the contract exists and see its available events.",
|
|
457
|
+
parameters: {
|
|
458
|
+
type: "object",
|
|
459
|
+
properties: {
|
|
460
|
+
address: {
|
|
461
|
+
type: "string",
|
|
462
|
+
description: "The contract address (0x-prefixed hex)."
|
|
463
|
+
},
|
|
464
|
+
networkId: {
|
|
465
|
+
type: "string",
|
|
466
|
+
description: 'The network ID, e.g. "ethereum-mainnet", "arbitrum-one".'
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
required: ["address", "networkId"]
|
|
470
|
+
},
|
|
471
|
+
execute: (args) => resolveContract(args),
|
|
472
|
+
confirm: (args) => `Resolving contract ${args.address} on ${args.networkId}`
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: "get_storage_slots",
|
|
476
|
+
description: "Get the known storage slot layout of a resolved contract. Returns variable names, types, and their storage slot positions. Useful for setting up storage-monitoring detections.",
|
|
477
|
+
parameters: {
|
|
478
|
+
type: "object",
|
|
479
|
+
properties: {
|
|
480
|
+
contractId: {
|
|
481
|
+
type: "string",
|
|
482
|
+
description: "The resolved contract ID (returned by resolve_contract)."
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
required: ["contractId"]
|
|
486
|
+
},
|
|
487
|
+
execute: (args) => getStorageSlots(args.contractId)
|
|
488
|
+
},
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
// Org Contracts
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
{
|
|
493
|
+
name: "list_org_contracts",
|
|
494
|
+
description: "List contracts registered to the user's organization. These are contracts the org has explicitly added for monitoring.",
|
|
495
|
+
parameters: {
|
|
496
|
+
type: "object",
|
|
497
|
+
properties: {
|
|
498
|
+
page: { type: "number", description: "Page number (1-based)." },
|
|
499
|
+
limit: { type: "number", description: "Results per page (default 20, max 100)." }
|
|
500
|
+
},
|
|
501
|
+
required: []
|
|
502
|
+
},
|
|
503
|
+
execute: (args) => listOrgContracts(args)
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: "register_org_contract",
|
|
507
|
+
description: "Register a contract to the user's organization. This resolves the contract and adds it to the org's contract list for easy reference when creating detections. The user should confirm before this runs.",
|
|
508
|
+
parameters: {
|
|
509
|
+
type: "object",
|
|
510
|
+
properties: {
|
|
511
|
+
address: {
|
|
512
|
+
type: "string",
|
|
513
|
+
description: "The contract address (0x-prefixed hex)."
|
|
514
|
+
},
|
|
515
|
+
networkId: {
|
|
516
|
+
type: "string",
|
|
517
|
+
description: 'The network ID, e.g. "ethereum-mainnet", "arbitrum-one".'
|
|
518
|
+
},
|
|
519
|
+
label: {
|
|
520
|
+
type: "string",
|
|
521
|
+
description: 'Optional human-readable label for the contract, e.g. "USDC Proxy".'
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
required: ["address", "networkId"]
|
|
525
|
+
},
|
|
526
|
+
execute: (args) => registerOrgContract(args),
|
|
527
|
+
confirm: (args) => `Registering contract ${args.address} on ${args.networkId}${args.label ? ` as "${args.label}"` : ""}`
|
|
528
|
+
},
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
// RPC Configs
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
{
|
|
533
|
+
name: "list_rpc_configs",
|
|
534
|
+
description: "List custom RPC endpoint configurations. Users can add their own RPC endpoints for better performance or to access private/premium nodes.",
|
|
535
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
536
|
+
execute: () => listRpcConfigs()
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: "add_rpc_config",
|
|
540
|
+
description: "Add a custom RPC endpoint for a specific network. This overrides the default public RPC for that network. The user should confirm before this runs.",
|
|
541
|
+
parameters: {
|
|
542
|
+
type: "object",
|
|
543
|
+
properties: {
|
|
544
|
+
networkId: {
|
|
545
|
+
type: "string",
|
|
546
|
+
description: 'The network ID to configure, e.g. "ethereum-mainnet", "arbitrum-one".'
|
|
547
|
+
},
|
|
548
|
+
rpcUrl: {
|
|
549
|
+
type: "string",
|
|
550
|
+
description: "The RPC endpoint URL (https or wss)."
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
required: ["networkId", "rpcUrl"]
|
|
554
|
+
},
|
|
555
|
+
execute: (args) => addRpcConfig(args),
|
|
556
|
+
confirm: (args) => `Adding custom RPC endpoint for ${args.networkId}: ${args.rpcUrl}`
|
|
557
|
+
}
|
|
558
|
+
];
|
|
559
|
+
|
|
560
|
+
// src/ai/tools.ts
|
|
561
|
+
var tools = registry.map((t) => ({
|
|
562
|
+
type: "function",
|
|
563
|
+
function: { name: t.name, description: t.description, parameters: t.parameters }
|
|
564
|
+
}));
|
|
565
|
+
|
|
566
|
+
// src/ai/executor.ts
|
|
567
|
+
var toolMap = new Map(registry.map((t) => [t.name, t]));
|
|
568
|
+
async function executeTool(name, args) {
|
|
569
|
+
const tool = toolMap.get(name);
|
|
570
|
+
if (!tool) return { error: `Unknown tool: ${name}` };
|
|
571
|
+
if (tool.confirm) {
|
|
572
|
+
printWarn(tool.confirm(args));
|
|
573
|
+
const ok = await confirm("Proceed?");
|
|
574
|
+
if (!ok) return { cancelled: true, message: "User cancelled the operation." };
|
|
575
|
+
}
|
|
576
|
+
return tool.execute(args);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ../../packages/shared/dist/templates/index.js
|
|
580
|
+
var MVP_TEMPLATES = [
|
|
581
|
+
// -------------------------------------------------------------------------
|
|
582
|
+
// 1. Large Transfer
|
|
583
|
+
// -------------------------------------------------------------------------
|
|
584
|
+
{
|
|
585
|
+
id: "tpl_large_transfer",
|
|
586
|
+
slug: "large-transfer",
|
|
587
|
+
name: "Large Transfer",
|
|
588
|
+
description: "Alert when an ERC-20 Transfer event moves more than a specified amount of tokens.",
|
|
589
|
+
category: "token-activity",
|
|
590
|
+
icon: "arrow-up-right",
|
|
591
|
+
severityDefault: "high",
|
|
592
|
+
tier: "free",
|
|
593
|
+
inputs: [
|
|
594
|
+
{
|
|
595
|
+
key: "threshold",
|
|
596
|
+
label: "Transfer threshold (token units)",
|
|
597
|
+
type: "number",
|
|
598
|
+
required: true,
|
|
599
|
+
min: 0,
|
|
600
|
+
help: "Transfers with a value above this amount will fire the alert."
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
key: "tokenAddress",
|
|
604
|
+
label: "Token contract address",
|
|
605
|
+
type: "address",
|
|
606
|
+
required: false,
|
|
607
|
+
help: "Leave blank to monitor the contract added to the detection."
|
|
608
|
+
}
|
|
609
|
+
],
|
|
610
|
+
ruleTemplates: [
|
|
611
|
+
{
|
|
612
|
+
type: "event-match",
|
|
613
|
+
config: {
|
|
614
|
+
eventSignature: "Transfer(address,address,uint256)",
|
|
615
|
+
eventName: "Transfer",
|
|
616
|
+
filter: {
|
|
617
|
+
field: "value",
|
|
618
|
+
op: ">",
|
|
619
|
+
value: "{{threshold}}"
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
],
|
|
624
|
+
isActive: true
|
|
625
|
+
},
|
|
626
|
+
// -------------------------------------------------------------------------
|
|
627
|
+
// 2. Fund Drainage
|
|
628
|
+
// -------------------------------------------------------------------------
|
|
629
|
+
{
|
|
630
|
+
id: "tpl_fund_drainage",
|
|
631
|
+
slug: "fund-drainage",
|
|
632
|
+
name: "Fund Drainage",
|
|
633
|
+
description: "Alert when the native or token balance of a contract drops by a specified percentage within a time window.",
|
|
634
|
+
category: "balance",
|
|
635
|
+
icon: "trending-down",
|
|
636
|
+
severityDefault: "critical",
|
|
637
|
+
tier: "free",
|
|
638
|
+
inputs: [
|
|
639
|
+
{
|
|
640
|
+
key: "dropPercent",
|
|
641
|
+
label: "Drop percentage",
|
|
642
|
+
type: "number",
|
|
643
|
+
required: true,
|
|
644
|
+
min: 1,
|
|
645
|
+
max: 100,
|
|
646
|
+
default: 50,
|
|
647
|
+
help: "Fire the alert when the balance decreases by at least this percentage."
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
key: "windowMinutes",
|
|
651
|
+
label: "Time window (minutes)",
|
|
652
|
+
type: "duration",
|
|
653
|
+
required: true,
|
|
654
|
+
default: 60,
|
|
655
|
+
min: 1,
|
|
656
|
+
help: "The rolling window over which the balance drop is measured."
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
key: "tokenAddress",
|
|
660
|
+
label: "Token contract (blank = native)",
|
|
661
|
+
type: "address",
|
|
662
|
+
required: false,
|
|
663
|
+
help: "Leave blank to track the native asset (ETH, MATIC, etc.)."
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
ruleTemplates: [
|
|
667
|
+
{
|
|
668
|
+
type: "balance-track",
|
|
669
|
+
config: {
|
|
670
|
+
asset: "{{tokenAddress}}",
|
|
671
|
+
windowMinutes: "{{windowMinutes}}",
|
|
672
|
+
condition: {
|
|
673
|
+
type: "percent_change",
|
|
674
|
+
value: "{{dropPercent}}"
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
],
|
|
679
|
+
isActive: true
|
|
680
|
+
},
|
|
681
|
+
// -------------------------------------------------------------------------
|
|
682
|
+
// 3. Balance Low
|
|
683
|
+
// -------------------------------------------------------------------------
|
|
684
|
+
{
|
|
685
|
+
id: "tpl_balance_low",
|
|
686
|
+
slug: "balance-low",
|
|
687
|
+
name: "Balance Low",
|
|
688
|
+
description: "Alert when the native or token balance of a contract falls below a fixed threshold.",
|
|
689
|
+
category: "balance",
|
|
690
|
+
icon: "alert-triangle",
|
|
691
|
+
severityDefault: "medium",
|
|
692
|
+
tier: "free",
|
|
693
|
+
inputs: [
|
|
694
|
+
{
|
|
695
|
+
key: "minBalance",
|
|
696
|
+
label: "Minimum balance",
|
|
697
|
+
type: "number",
|
|
698
|
+
required: true,
|
|
699
|
+
min: 0,
|
|
700
|
+
help: "Fire the alert when the balance drops below this value."
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
key: "tokenAddress",
|
|
704
|
+
label: "Token contract (blank = native)",
|
|
705
|
+
type: "address",
|
|
706
|
+
required: false,
|
|
707
|
+
help: "Leave blank to track the native asset (ETH, MATIC, etc.)."
|
|
708
|
+
}
|
|
709
|
+
],
|
|
710
|
+
ruleTemplates: [
|
|
711
|
+
{
|
|
712
|
+
type: "balance-track",
|
|
713
|
+
config: {
|
|
714
|
+
asset: "{{tokenAddress}}",
|
|
715
|
+
condition: {
|
|
716
|
+
op: "<",
|
|
717
|
+
value: "{{minBalance}}"
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
],
|
|
722
|
+
isActive: true
|
|
723
|
+
},
|
|
724
|
+
// -------------------------------------------------------------------------
|
|
725
|
+
// 4. Repeated Transfer
|
|
726
|
+
// -------------------------------------------------------------------------
|
|
727
|
+
{
|
|
728
|
+
id: "tpl_repeated_transfer",
|
|
729
|
+
slug: "repeated-transfer",
|
|
730
|
+
name: "Repeated Transfer",
|
|
731
|
+
description: "Alert when a specified number of Transfer events to the same recipient occur within a time window.",
|
|
732
|
+
category: "token-activity",
|
|
733
|
+
icon: "repeat",
|
|
734
|
+
severityDefault: "medium",
|
|
735
|
+
tier: "pro",
|
|
736
|
+
inputs: [
|
|
737
|
+
{
|
|
738
|
+
key: "countThreshold",
|
|
739
|
+
label: "Transfer count",
|
|
740
|
+
type: "number",
|
|
741
|
+
required: true,
|
|
742
|
+
min: 2,
|
|
743
|
+
default: 5,
|
|
744
|
+
help: "Number of transfers to the same address that triggers the alert."
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
key: "windowMinutes",
|
|
748
|
+
label: "Time window (minutes)",
|
|
749
|
+
type: "duration",
|
|
750
|
+
required: true,
|
|
751
|
+
default: 60,
|
|
752
|
+
min: 1,
|
|
753
|
+
help: "The rolling window in which transfers are counted."
|
|
754
|
+
}
|
|
755
|
+
],
|
|
756
|
+
ruleTemplates: [
|
|
757
|
+
{
|
|
758
|
+
type: "windowed-count",
|
|
759
|
+
config: {
|
|
760
|
+
eventSignature: "Transfer(address,address,uint256)",
|
|
761
|
+
eventName: "Transfer",
|
|
762
|
+
groupByField: "to",
|
|
763
|
+
windowMinutes: "{{windowMinutes}}",
|
|
764
|
+
condition: {
|
|
765
|
+
op: ">=",
|
|
766
|
+
value: "{{countThreshold}}"
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
],
|
|
771
|
+
isActive: true
|
|
772
|
+
},
|
|
773
|
+
// -------------------------------------------------------------------------
|
|
774
|
+
// 5. Ownership Transfer
|
|
775
|
+
// -------------------------------------------------------------------------
|
|
776
|
+
{
|
|
777
|
+
id: "tpl_ownership_transfer",
|
|
778
|
+
slug: "ownership-transfer",
|
|
779
|
+
name: "Ownership Transfer",
|
|
780
|
+
description: "Alert when ownership changes or a transfer is initiated (OpenZeppelin Ownable / Ownable2Step).",
|
|
781
|
+
category: "governance",
|
|
782
|
+
icon: "shield",
|
|
783
|
+
severityDefault: "critical",
|
|
784
|
+
tier: "free",
|
|
785
|
+
inputs: [],
|
|
786
|
+
ruleTemplates: [
|
|
787
|
+
{
|
|
788
|
+
type: "event-match",
|
|
789
|
+
config: {
|
|
790
|
+
eventSignature: "OwnershipTransferred(address,address)",
|
|
791
|
+
eventName: "OwnershipTransferred"
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
type: "event-match",
|
|
796
|
+
config: {
|
|
797
|
+
eventSignature: "OwnershipTransferStarted(address,address)",
|
|
798
|
+
eventName: "OwnershipTransferStarted"
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
],
|
|
802
|
+
isActive: true
|
|
803
|
+
},
|
|
804
|
+
// -------------------------------------------------------------------------
|
|
805
|
+
// 6. Role Change
|
|
806
|
+
// -------------------------------------------------------------------------
|
|
807
|
+
{
|
|
808
|
+
id: "tpl_role_change",
|
|
809
|
+
slug: "role-change",
|
|
810
|
+
name: "Role Change",
|
|
811
|
+
description: "Alert when a RoleGranted or RoleRevoked event is emitted (OpenZeppelin AccessControl).",
|
|
812
|
+
category: "governance",
|
|
813
|
+
icon: "users",
|
|
814
|
+
severityDefault: "high",
|
|
815
|
+
tier: "free",
|
|
816
|
+
inputs: [
|
|
817
|
+
{
|
|
818
|
+
key: "roleHash",
|
|
819
|
+
label: "Role identifier (bytes32, optional)",
|
|
820
|
+
type: "text",
|
|
821
|
+
required: false,
|
|
822
|
+
help: "Restrict to a specific role hash. Leave blank to match all roles."
|
|
823
|
+
}
|
|
824
|
+
],
|
|
825
|
+
ruleTemplates: [
|
|
826
|
+
{
|
|
827
|
+
type: "event-match",
|
|
828
|
+
config: {
|
|
829
|
+
eventSignature: "RoleGranted(bytes32,address,address)",
|
|
830
|
+
eventName: "RoleGranted",
|
|
831
|
+
filter: {
|
|
832
|
+
field: "role",
|
|
833
|
+
op: "==",
|
|
834
|
+
value: "{{roleHash}}",
|
|
835
|
+
skipIfEmpty: true
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
type: "event-match",
|
|
841
|
+
config: {
|
|
842
|
+
eventSignature: "RoleRevoked(bytes32,address,address)",
|
|
843
|
+
eventName: "RoleRevoked",
|
|
844
|
+
filter: {
|
|
845
|
+
field: "role",
|
|
846
|
+
op: "==",
|
|
847
|
+
value: "{{roleHash}}",
|
|
848
|
+
skipIfEmpty: true
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
],
|
|
853
|
+
isActive: true
|
|
854
|
+
},
|
|
855
|
+
// -------------------------------------------------------------------------
|
|
856
|
+
// 7. Proxy Upgrade
|
|
857
|
+
// -------------------------------------------------------------------------
|
|
858
|
+
{
|
|
859
|
+
id: "tpl_proxy_upgrade",
|
|
860
|
+
slug: "proxy-upgrade",
|
|
861
|
+
name: "Proxy Upgrade",
|
|
862
|
+
description: "Alert when the Upgraded event is emitted on a UUPS or Transparent proxy.",
|
|
863
|
+
category: "governance",
|
|
864
|
+
icon: "zap",
|
|
865
|
+
severityDefault: "critical",
|
|
866
|
+
tier: "free",
|
|
867
|
+
inputs: [],
|
|
868
|
+
ruleTemplates: [
|
|
869
|
+
{
|
|
870
|
+
type: "event-match",
|
|
871
|
+
config: {
|
|
872
|
+
eventSignature: "Upgraded(address)",
|
|
873
|
+
eventName: "Upgraded"
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
],
|
|
877
|
+
isActive: true
|
|
878
|
+
},
|
|
879
|
+
// -------------------------------------------------------------------------
|
|
880
|
+
// 8. Multisig Signer Change
|
|
881
|
+
// -------------------------------------------------------------------------
|
|
882
|
+
{
|
|
883
|
+
id: "tpl_multisig_signer",
|
|
884
|
+
slug: "multisig-signer",
|
|
885
|
+
name: "Multisig Signer Change",
|
|
886
|
+
description: "Alert when an owner is added or removed on a Gnosis Safe (AddedOwner / RemovedOwner).",
|
|
887
|
+
category: "governance",
|
|
888
|
+
icon: "key",
|
|
889
|
+
severityDefault: "high",
|
|
890
|
+
tier: "free",
|
|
891
|
+
inputs: [],
|
|
892
|
+
ruleTemplates: [
|
|
893
|
+
{
|
|
894
|
+
type: "event-match",
|
|
895
|
+
config: {
|
|
896
|
+
eventSignature: "AddedOwner(address)",
|
|
897
|
+
eventName: "AddedOwner"
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
type: "event-match",
|
|
902
|
+
config: {
|
|
903
|
+
eventSignature: "RemovedOwner(address)",
|
|
904
|
+
eventName: "RemovedOwner"
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
],
|
|
908
|
+
isActive: true
|
|
909
|
+
},
|
|
910
|
+
// -------------------------------------------------------------------------
|
|
911
|
+
// 9. Pause State
|
|
912
|
+
// -------------------------------------------------------------------------
|
|
913
|
+
{
|
|
914
|
+
id: "tpl_pause_state",
|
|
915
|
+
slug: "pause-state",
|
|
916
|
+
name: "Pause State Change",
|
|
917
|
+
description: "Alert when a Paused or Unpaused event is emitted (OpenZeppelin Pausable).",
|
|
918
|
+
category: "governance",
|
|
919
|
+
icon: "pause-circle",
|
|
920
|
+
severityDefault: "high",
|
|
921
|
+
tier: "free",
|
|
922
|
+
inputs: [],
|
|
923
|
+
ruleTemplates: [
|
|
924
|
+
{
|
|
925
|
+
type: "event-match",
|
|
926
|
+
config: {
|
|
927
|
+
eventSignature: "Paused(address)",
|
|
928
|
+
eventName: "Paused"
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
type: "event-match",
|
|
933
|
+
config: {
|
|
934
|
+
eventSignature: "Unpaused(address)",
|
|
935
|
+
eventName: "Unpaused"
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
],
|
|
939
|
+
isActive: true
|
|
940
|
+
},
|
|
941
|
+
// -------------------------------------------------------------------------
|
|
942
|
+
// 10. Proxy Upgrade (Storage Slot)
|
|
943
|
+
// -------------------------------------------------------------------------
|
|
944
|
+
{
|
|
945
|
+
id: "tpl_proxy_upgrade_slot",
|
|
946
|
+
slug: "proxy-upgrade-slot",
|
|
947
|
+
name: "Proxy Upgrade (Storage Slot)",
|
|
948
|
+
description: "Poll the ERC-1967 implementation slot directly to detect proxy upgrades \u2014 catches silent upgrades that bypass the Upgraded event.",
|
|
949
|
+
category: "governance",
|
|
950
|
+
icon: "eye",
|
|
951
|
+
severityDefault: "critical",
|
|
952
|
+
tier: "free",
|
|
953
|
+
inputs: [
|
|
954
|
+
{
|
|
955
|
+
key: "pollIntervalMs",
|
|
956
|
+
label: "Poll interval (seconds)",
|
|
957
|
+
type: "number",
|
|
958
|
+
required: false,
|
|
959
|
+
default: 60,
|
|
960
|
+
min: 10,
|
|
961
|
+
help: "How often to read the storage slot (in seconds)."
|
|
962
|
+
}
|
|
963
|
+
],
|
|
964
|
+
ruleTemplates: [
|
|
965
|
+
{
|
|
966
|
+
type: "state-poll",
|
|
967
|
+
config: {
|
|
968
|
+
slot: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
|
|
969
|
+
poll_interval_ms: "{{pollIntervalMs:seconds_to_ms}}",
|
|
970
|
+
condition: {
|
|
971
|
+
type: "changed"
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
],
|
|
976
|
+
isActive: true
|
|
977
|
+
},
|
|
978
|
+
// -------------------------------------------------------------------------
|
|
979
|
+
// 11. Custom Storage Slot
|
|
980
|
+
// -------------------------------------------------------------------------
|
|
981
|
+
{
|
|
982
|
+
id: "tpl_custom_storage_slot",
|
|
983
|
+
slug: "custom-storage-slot",
|
|
984
|
+
name: "Custom Storage Slot",
|
|
985
|
+
description: "Monitor any EVM storage slot for changes or threshold crossings. Use for protocol parameters, implementation slots, or any on-chain variable.",
|
|
986
|
+
category: "custom",
|
|
987
|
+
icon: "database",
|
|
988
|
+
severityDefault: "high",
|
|
989
|
+
tier: "pro",
|
|
990
|
+
inputs: [
|
|
991
|
+
{
|
|
992
|
+
key: "slot",
|
|
993
|
+
label: "Storage slot (hex)",
|
|
994
|
+
type: "text",
|
|
995
|
+
required: true,
|
|
996
|
+
help: "The 32-byte hex slot to monitor, e.g. 0x360894\u2026bbc."
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
key: "conditionType",
|
|
1000
|
+
label: "Condition",
|
|
1001
|
+
type: "text",
|
|
1002
|
+
required: true,
|
|
1003
|
+
default: "changed",
|
|
1004
|
+
help: 'Condition type: "changed", "threshold_above", or "threshold_below".'
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
key: "conditionValue",
|
|
1008
|
+
label: "Threshold value",
|
|
1009
|
+
type: "number",
|
|
1010
|
+
required: false,
|
|
1011
|
+
help: "Required for threshold conditions. The value to compare against.",
|
|
1012
|
+
showIf: "conditionType"
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
key: "pollIntervalMs",
|
|
1016
|
+
label: "Poll interval (seconds)",
|
|
1017
|
+
type: "number",
|
|
1018
|
+
required: false,
|
|
1019
|
+
default: 60,
|
|
1020
|
+
min: 10,
|
|
1021
|
+
help: "How often to read the storage slot (in seconds)."
|
|
1022
|
+
}
|
|
1023
|
+
],
|
|
1024
|
+
ruleTemplates: [
|
|
1025
|
+
{
|
|
1026
|
+
type: "state-poll",
|
|
1027
|
+
config: {
|
|
1028
|
+
slot: "{{slot}}",
|
|
1029
|
+
poll_interval_ms: "{{pollIntervalMs:seconds_to_ms}}",
|
|
1030
|
+
condition: {
|
|
1031
|
+
type: "{{conditionType}}",
|
|
1032
|
+
value: "{{conditionValue}}"
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
],
|
|
1037
|
+
isActive: true
|
|
1038
|
+
},
|
|
1039
|
+
// -------------------------------------------------------------------------
|
|
1040
|
+
// 12. Storage Anomaly
|
|
1041
|
+
// -------------------------------------------------------------------------
|
|
1042
|
+
{
|
|
1043
|
+
id: "tpl_storage_anomaly",
|
|
1044
|
+
slug: "storage-anomaly",
|
|
1045
|
+
name: "Storage Anomaly",
|
|
1046
|
+
description: "Detect when a storage variable's value suddenly deviates from its rolling average. Useful for spotting parameter manipulation in DeFi protocols.",
|
|
1047
|
+
category: "governance",
|
|
1048
|
+
icon: "activity",
|
|
1049
|
+
severityDefault: "high",
|
|
1050
|
+
tier: "pro",
|
|
1051
|
+
inputs: [
|
|
1052
|
+
{
|
|
1053
|
+
key: "slot",
|
|
1054
|
+
label: "Storage slot (hex)",
|
|
1055
|
+
type: "text",
|
|
1056
|
+
required: true,
|
|
1057
|
+
help: "The 32-byte hex slot to monitor."
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
key: "percentThreshold",
|
|
1061
|
+
label: "Deviation threshold (%)",
|
|
1062
|
+
type: "number",
|
|
1063
|
+
required: true,
|
|
1064
|
+
default: 200,
|
|
1065
|
+
min: 1,
|
|
1066
|
+
help: "Alert when the current value deviates from the rolling mean by more than this percentage."
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
key: "pollIntervalMs",
|
|
1070
|
+
label: "Poll interval (seconds)",
|
|
1071
|
+
type: "number",
|
|
1072
|
+
required: false,
|
|
1073
|
+
default: 60,
|
|
1074
|
+
min: 10,
|
|
1075
|
+
help: "How often to read the storage slot (in seconds)."
|
|
1076
|
+
}
|
|
1077
|
+
],
|
|
1078
|
+
ruleTemplates: [
|
|
1079
|
+
{
|
|
1080
|
+
type: "state-poll",
|
|
1081
|
+
config: {
|
|
1082
|
+
slot: "{{slot}}",
|
|
1083
|
+
poll_interval_ms: "{{pollIntervalMs:seconds_to_ms}}",
|
|
1084
|
+
condition: {
|
|
1085
|
+
type: "windowed_percent_change",
|
|
1086
|
+
percentThreshold: "{{percentThreshold}}"
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
],
|
|
1091
|
+
isActive: true
|
|
1092
|
+
},
|
|
1093
|
+
// -------------------------------------------------------------------------
|
|
1094
|
+
// 13. Native Balance Anomaly (Internal Transfer Detection)
|
|
1095
|
+
// -------------------------------------------------------------------------
|
|
1096
|
+
{
|
|
1097
|
+
id: "tpl_native_balance_anomaly",
|
|
1098
|
+
slug: "native-balance-anomaly",
|
|
1099
|
+
name: "Native Balance Anomaly",
|
|
1100
|
+
description: "Detect significant native balance changes that may indicate internal transactions (ETH moved between contracts without Transfer events).",
|
|
1101
|
+
category: "balance",
|
|
1102
|
+
icon: "eye-off",
|
|
1103
|
+
severityDefault: "high",
|
|
1104
|
+
tier: "free",
|
|
1105
|
+
inputs: [
|
|
1106
|
+
{
|
|
1107
|
+
key: "dropPercent",
|
|
1108
|
+
label: "Change threshold (%)",
|
|
1109
|
+
type: "number",
|
|
1110
|
+
required: true,
|
|
1111
|
+
default: 25,
|
|
1112
|
+
min: 1,
|
|
1113
|
+
max: 100,
|
|
1114
|
+
help: "Alert when native balance changes by at least this percentage within the time window."
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
key: "windowMinutes",
|
|
1118
|
+
label: "Time window (minutes)",
|
|
1119
|
+
type: "duration",
|
|
1120
|
+
required: true,
|
|
1121
|
+
default: 30,
|
|
1122
|
+
min: 1,
|
|
1123
|
+
help: "The rolling window over which the balance change is measured."
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
key: "pollIntervalMs",
|
|
1127
|
+
label: "Poll interval (seconds)",
|
|
1128
|
+
type: "number",
|
|
1129
|
+
required: false,
|
|
1130
|
+
default: 30,
|
|
1131
|
+
min: 10,
|
|
1132
|
+
help: "How often to check the native balance. Shorter intervals catch more internal transfers."
|
|
1133
|
+
}
|
|
1134
|
+
],
|
|
1135
|
+
ruleTemplates: [
|
|
1136
|
+
{
|
|
1137
|
+
type: "balance-track",
|
|
1138
|
+
config: {
|
|
1139
|
+
poll_interval_ms: "{{pollIntervalMs:seconds_to_ms}}",
|
|
1140
|
+
windowMinutes: "{{windowMinutes}}",
|
|
1141
|
+
condition: {
|
|
1142
|
+
type: "percent_change",
|
|
1143
|
+
value: "{{dropPercent}}",
|
|
1144
|
+
bidirectional: true
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
],
|
|
1149
|
+
isActive: true
|
|
1150
|
+
},
|
|
1151
|
+
// -------------------------------------------------------------------------
|
|
1152
|
+
// 14. Custom Event Frequency (Windowed Count)
|
|
1153
|
+
// -------------------------------------------------------------------------
|
|
1154
|
+
{
|
|
1155
|
+
id: "tpl_custom_windowed_count",
|
|
1156
|
+
slug: "custom-windowed-count",
|
|
1157
|
+
name: "Custom Event Frequency",
|
|
1158
|
+
description: "Alert when a specific event fires more than N times within a rolling time window. Optionally group by a decoded event parameter.",
|
|
1159
|
+
category: "custom",
|
|
1160
|
+
icon: "activity",
|
|
1161
|
+
severityDefault: "medium",
|
|
1162
|
+
tier: "pro",
|
|
1163
|
+
inputs: [
|
|
1164
|
+
{
|
|
1165
|
+
key: "eventSignature",
|
|
1166
|
+
label: "Event signature",
|
|
1167
|
+
type: "text",
|
|
1168
|
+
required: true,
|
|
1169
|
+
help: 'Full Solidity event signature, e.g. "WithdrawInitiated(address,uint256)".'
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
key: "eventName",
|
|
1173
|
+
label: "Event name",
|
|
1174
|
+
type: "text",
|
|
1175
|
+
required: true,
|
|
1176
|
+
help: "Human-readable name shown in alerts."
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
key: "countThreshold",
|
|
1180
|
+
label: "Count threshold",
|
|
1181
|
+
type: "number",
|
|
1182
|
+
required: true,
|
|
1183
|
+
default: 5,
|
|
1184
|
+
min: 2,
|
|
1185
|
+
help: "Alert when this many events occur within the window."
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
key: "windowMinutes",
|
|
1189
|
+
label: "Time window (minutes)",
|
|
1190
|
+
type: "duration",
|
|
1191
|
+
required: true,
|
|
1192
|
+
default: 60,
|
|
1193
|
+
min: 1,
|
|
1194
|
+
help: "Rolling time window in minutes."
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
key: "groupByField",
|
|
1198
|
+
label: "Group by parameter",
|
|
1199
|
+
type: "text",
|
|
1200
|
+
required: false,
|
|
1201
|
+
help: 'Optional: decoded event parameter to group counts by (e.g. "to", "sender"). Leave blank for global count.'
|
|
1202
|
+
}
|
|
1203
|
+
],
|
|
1204
|
+
ruleTemplates: [
|
|
1205
|
+
{
|
|
1206
|
+
type: "windowed-count",
|
|
1207
|
+
config: {
|
|
1208
|
+
eventSignature: "{{eventSignature}}",
|
|
1209
|
+
eventName: "{{eventName}}",
|
|
1210
|
+
groupByField: "{{groupByField}}",
|
|
1211
|
+
windowMinutes: "{{windowMinutes}}",
|
|
1212
|
+
condition: {
|
|
1213
|
+
op: ">=",
|
|
1214
|
+
value: "{{countThreshold}}"
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
],
|
|
1219
|
+
isActive: true
|
|
1220
|
+
},
|
|
1221
|
+
// -------------------------------------------------------------------------
|
|
1222
|
+
// 15. Event Frequency Spike
|
|
1223
|
+
// -------------------------------------------------------------------------
|
|
1224
|
+
{
|
|
1225
|
+
id: "tpl_event_frequency_spike",
|
|
1226
|
+
slug: "event-frequency-spike",
|
|
1227
|
+
name: "Event Frequency Spike",
|
|
1228
|
+
description: "Alert when an event's firing rate increases by more than X% compared to a baseline period.",
|
|
1229
|
+
category: "custom",
|
|
1230
|
+
icon: "trending-up",
|
|
1231
|
+
severityDefault: "high",
|
|
1232
|
+
tier: "pro",
|
|
1233
|
+
inputs: [
|
|
1234
|
+
{
|
|
1235
|
+
key: "eventSignature",
|
|
1236
|
+
label: "Event signature",
|
|
1237
|
+
type: "text",
|
|
1238
|
+
required: true,
|
|
1239
|
+
help: 'Full Solidity event signature, e.g. "WithdrawInitiated(address,uint256)".'
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
key: "eventName",
|
|
1243
|
+
label: "Event name",
|
|
1244
|
+
type: "text",
|
|
1245
|
+
required: true,
|
|
1246
|
+
help: "Human-readable name shown in alerts."
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
key: "observationMinutes",
|
|
1250
|
+
label: "Observation window (minutes)",
|
|
1251
|
+
type: "duration",
|
|
1252
|
+
required: true,
|
|
1253
|
+
default: 5,
|
|
1254
|
+
min: 1,
|
|
1255
|
+
help: "Short rolling window to measure current event rate."
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
key: "baselineMinutes",
|
|
1259
|
+
label: "Baseline window (minutes)",
|
|
1260
|
+
type: "duration",
|
|
1261
|
+
required: true,
|
|
1262
|
+
default: 60,
|
|
1263
|
+
min: 10,
|
|
1264
|
+
help: "Longer window used to compute the average baseline rate."
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
key: "increasePercent",
|
|
1268
|
+
label: "Spike threshold (%)",
|
|
1269
|
+
type: "number",
|
|
1270
|
+
required: true,
|
|
1271
|
+
default: 200,
|
|
1272
|
+
min: 50,
|
|
1273
|
+
help: "Alert when the current rate exceeds the baseline average by at least this percentage."
|
|
1274
|
+
},
|
|
1275
|
+
{
|
|
1276
|
+
key: "minBaselineCount",
|
|
1277
|
+
label: "Minimum baseline events",
|
|
1278
|
+
type: "number",
|
|
1279
|
+
required: true,
|
|
1280
|
+
default: 3,
|
|
1281
|
+
min: 1,
|
|
1282
|
+
help: "Minimum events in the baseline window to avoid false positives from near-zero baselines."
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
key: "groupByField",
|
|
1286
|
+
label: "Group by parameter",
|
|
1287
|
+
type: "text",
|
|
1288
|
+
required: false,
|
|
1289
|
+
help: 'Optional: decoded event parameter to group spike detection by (e.g. "to", "sender"). Leave blank for global rate.'
|
|
1290
|
+
}
|
|
1291
|
+
],
|
|
1292
|
+
ruleTemplates: [
|
|
1293
|
+
{
|
|
1294
|
+
type: "windowed-spike",
|
|
1295
|
+
config: {
|
|
1296
|
+
eventSignature: "{{eventSignature}}",
|
|
1297
|
+
eventName: "{{eventName}}",
|
|
1298
|
+
groupByField: "{{groupByField}}",
|
|
1299
|
+
observationMinutes: "{{observationMinutes}}",
|
|
1300
|
+
baselineMinutes: "{{baselineMinutes}}",
|
|
1301
|
+
increasePercent: "{{increasePercent}}",
|
|
1302
|
+
minBaselineCount: "{{minBaselineCount}}"
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
],
|
|
1306
|
+
isActive: true
|
|
1307
|
+
},
|
|
1308
|
+
// -------------------------------------------------------------------------
|
|
1309
|
+
// 16. Custom Event
|
|
1310
|
+
// -------------------------------------------------------------------------
|
|
1311
|
+
{
|
|
1312
|
+
id: "tpl_custom_event",
|
|
1313
|
+
slug: "custom-event",
|
|
1314
|
+
name: "Custom Event",
|
|
1315
|
+
description: "Watch for any event from the contract ABI. Select the event and optionally add a filter on one of its parameters.",
|
|
1316
|
+
category: "custom",
|
|
1317
|
+
icon: "code",
|
|
1318
|
+
severityDefault: "medium",
|
|
1319
|
+
tier: "free",
|
|
1320
|
+
inputs: [
|
|
1321
|
+
{
|
|
1322
|
+
key: "eventSignature",
|
|
1323
|
+
label: "Event signature",
|
|
1324
|
+
type: "text",
|
|
1325
|
+
required: true,
|
|
1326
|
+
help: 'Full Solidity event signature, e.g. "Transfer(address,address,uint256)".'
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
key: "eventName",
|
|
1330
|
+
label: "Event name",
|
|
1331
|
+
type: "text",
|
|
1332
|
+
required: true,
|
|
1333
|
+
help: "Human-readable name shown in alerts."
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
key: "filterField",
|
|
1337
|
+
label: "Filter parameter name",
|
|
1338
|
+
type: "text",
|
|
1339
|
+
required: false,
|
|
1340
|
+
help: "Optional: parameter name to filter on."
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
key: "filterOp",
|
|
1344
|
+
label: "Filter operator",
|
|
1345
|
+
type: "text",
|
|
1346
|
+
required: false,
|
|
1347
|
+
default: "==",
|
|
1348
|
+
help: "Comparison operator (>, <, >=, <=, ==, !=).",
|
|
1349
|
+
showIf: "filterField"
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
key: "filterValue",
|
|
1353
|
+
label: "Filter value",
|
|
1354
|
+
type: "text",
|
|
1355
|
+
required: false,
|
|
1356
|
+
help: "Value to compare the parameter against.",
|
|
1357
|
+
showIf: "filterField"
|
|
1358
|
+
}
|
|
1359
|
+
],
|
|
1360
|
+
ruleTemplates: [
|
|
1361
|
+
{
|
|
1362
|
+
type: "event-match",
|
|
1363
|
+
config: {
|
|
1364
|
+
eventSignature: "{{eventSignature}}",
|
|
1365
|
+
eventName: "{{eventName}}",
|
|
1366
|
+
filter: {
|
|
1367
|
+
field: "{{filterField}}",
|
|
1368
|
+
op: "{{filterOp}}",
|
|
1369
|
+
value: "{{filterValue}}",
|
|
1370
|
+
skipIfEmpty: true
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
],
|
|
1375
|
+
isActive: true
|
|
1376
|
+
}
|
|
1377
|
+
];
|
|
1378
|
+
|
|
1379
|
+
// ../../packages/shared/dist/chains.js
|
|
1380
|
+
var ETHEREUM_MAINNET = {
|
|
1381
|
+
id: "ethereum-mainnet",
|
|
1382
|
+
name: "Ethereum Mainnet",
|
|
1383
|
+
slug: "ethereum",
|
|
1384
|
+
chainId: 1,
|
|
1385
|
+
rpcUrl: "https://ethereum-rpc.publicnode.com",
|
|
1386
|
+
blockTimeMs: 12e3,
|
|
1387
|
+
explorerUrl: "https://etherscan.io",
|
|
1388
|
+
explorerApi: "https://api.etherscan.io/api",
|
|
1389
|
+
isActive: true
|
|
1390
|
+
};
|
|
1391
|
+
var ETHEREUM_SEPOLIA = {
|
|
1392
|
+
id: "ethereum-sepolia",
|
|
1393
|
+
name: "Ethereum Sepolia",
|
|
1394
|
+
slug: "sepolia",
|
|
1395
|
+
chainId: 11155111,
|
|
1396
|
+
rpcUrl: "https://rpc.sepolia.org",
|
|
1397
|
+
blockTimeMs: 12e3,
|
|
1398
|
+
explorerUrl: "https://sepolia.etherscan.io",
|
|
1399
|
+
explorerApi: "https://api-sepolia.etherscan.io/api",
|
|
1400
|
+
isActive: true
|
|
1401
|
+
};
|
|
1402
|
+
var ARBITRUM_ONE = {
|
|
1403
|
+
id: "arbitrum-one",
|
|
1404
|
+
name: "Arbitrum One",
|
|
1405
|
+
slug: "arbitrum",
|
|
1406
|
+
chainId: 42161,
|
|
1407
|
+
rpcUrl: "https://arb1.arbitrum.io/rpc",
|
|
1408
|
+
blockTimeMs: 250,
|
|
1409
|
+
explorerUrl: "https://arbiscan.io",
|
|
1410
|
+
explorerApi: "https://api.arbiscan.io/api",
|
|
1411
|
+
isActive: true
|
|
1412
|
+
};
|
|
1413
|
+
var BASE = {
|
|
1414
|
+
id: "base-mainnet",
|
|
1415
|
+
name: "Base",
|
|
1416
|
+
slug: "base",
|
|
1417
|
+
chainId: 8453,
|
|
1418
|
+
rpcUrl: "https://mainnet.base.org",
|
|
1419
|
+
blockTimeMs: 2e3,
|
|
1420
|
+
explorerUrl: "https://basescan.org",
|
|
1421
|
+
explorerApi: "https://api.basescan.org/api",
|
|
1422
|
+
isActive: true
|
|
1423
|
+
};
|
|
1424
|
+
var POLYGON = {
|
|
1425
|
+
id: "polygon-mainnet",
|
|
1426
|
+
name: "Polygon",
|
|
1427
|
+
slug: "polygon",
|
|
1428
|
+
chainId: 137,
|
|
1429
|
+
rpcUrl: "https://polygon-rpc.com",
|
|
1430
|
+
blockTimeMs: 2e3,
|
|
1431
|
+
explorerUrl: "https://polygonscan.com",
|
|
1432
|
+
explorerApi: "https://api.polygonscan.com/api",
|
|
1433
|
+
isActive: true
|
|
1434
|
+
};
|
|
1435
|
+
var SUPPORTED_CHAINS = {
|
|
1436
|
+
[ETHEREUM_MAINNET.slug]: ETHEREUM_MAINNET,
|
|
1437
|
+
[ETHEREUM_SEPOLIA.slug]: ETHEREUM_SEPOLIA,
|
|
1438
|
+
[ARBITRUM_ONE.slug]: ARBITRUM_ONE,
|
|
1439
|
+
[BASE.slug]: BASE,
|
|
1440
|
+
[POLYGON.slug]: POLYGON
|
|
1441
|
+
};
|
|
1442
|
+
var SUPPORTED_CHAINS_LIST = Object.values(SUPPORTED_CHAINS);
|
|
1443
|
+
|
|
1444
|
+
// src/ai/system-prompt.ts
|
|
1445
|
+
function buildSystemPrompt() {
|
|
1446
|
+
const templateList = MVP_TEMPLATES.map((t) => {
|
|
1447
|
+
const inputs = t.inputs.length > 0 ? t.inputs.map(
|
|
1448
|
+
(i) => ` - ${i.key} (${i.type}${i.required ? ", required" : ", optional"}): ${i.help || i.label}`
|
|
1449
|
+
).join("\n") : " (no inputs required)";
|
|
1450
|
+
return ` ${t.slug}: ${t.name}
|
|
1451
|
+
${t.description}
|
|
1452
|
+
Category: ${t.category} | Default severity: ${t.severityDefault} | Tier: ${t.tier}
|
|
1453
|
+
Inputs:
|
|
1454
|
+
${inputs}`;
|
|
1455
|
+
}).join("\n\n");
|
|
1456
|
+
const chainList = SUPPORTED_CHAINS_LIST.map(
|
|
1457
|
+
(c) => ` - ${c.slug} (${c.name}, chain ID ${c.chainId}, network ID "${c.id}")`
|
|
1458
|
+
).join("\n");
|
|
1459
|
+
return `You are ChainAlert Assistant, an AI that helps users manage blockchain monitoring through natural language.
|
|
1460
|
+
|
|
1461
|
+
## What You Can Do
|
|
1462
|
+
|
|
1463
|
+
You help users with:
|
|
1464
|
+
- Creating, viewing, updating, pausing, resuming, and deleting detections (blockchain monitors)
|
|
1465
|
+
- Browsing detection templates to find the right monitoring setup
|
|
1466
|
+
- Viewing and analyzing alerts that have been triggered
|
|
1467
|
+
- Managing notification channels (Slack, Discord, email, webhook, Telegram, PagerDuty)
|
|
1468
|
+
- Resolving and registering smart contracts
|
|
1469
|
+
- Viewing blockchain events and storage state changes
|
|
1470
|
+
- Configuring custom RPC endpoints
|
|
1471
|
+
- Providing general guidance on blockchain monitoring best practices
|
|
1472
|
+
|
|
1473
|
+
## Supported Networks
|
|
1474
|
+
|
|
1475
|
+
${chainList}
|
|
1476
|
+
|
|
1477
|
+
When users mention a chain by name (e.g. "Ethereum", "Arbitrum"), map it to the correct network ID (e.g. "ethereum-mainnet", "arbitrum-one"). For testnets, users must specify explicitly (e.g. "Sepolia" maps to "ethereum-sepolia").
|
|
1478
|
+
|
|
1479
|
+
## Detection Templates
|
|
1480
|
+
|
|
1481
|
+
These are the available monitoring templates users can activate:
|
|
1482
|
+
|
|
1483
|
+
${templateList}
|
|
1484
|
+
|
|
1485
|
+
## Key Concepts
|
|
1486
|
+
|
|
1487
|
+
- **Detection**: An active monitoring configuration created from a template. It watches a specific contract on a specific network and fires alerts when conditions are met.
|
|
1488
|
+
- **Template**: A pre-built detection blueprint. Users pick a template and fill in its inputs (like threshold values, addresses) to create a detection.
|
|
1489
|
+
- **Alert**: A notification generated when a detection's rules fire. Alerts have severity levels: info, low, medium, high, critical.
|
|
1490
|
+
- **Channel**: A notification destination (Slack, Discord, email, webhook, Telegram, PagerDuty). Detections can be linked to one or more channels.
|
|
1491
|
+
- **Event**: A raw on-chain event (e.g. Transfer, Approval) captured by the monitoring pipeline.
|
|
1492
|
+
- **State Change**: A change in a monitored storage slot's value, captured by state-polling detections.
|
|
1493
|
+
- **Org Contract**: A smart contract registered to the user's organization for easy reference.
|
|
1494
|
+
|
|
1495
|
+
## Guidelines
|
|
1496
|
+
|
|
1497
|
+
- Be concise and direct. Users are developers who prefer efficient communication.
|
|
1498
|
+
- Always use the available tools to fetch real data rather than making assumptions.
|
|
1499
|
+
- When creating or modifying resources, summarize what you are about to do so the user can confirm.
|
|
1500
|
+
- If a user's request is ambiguous, ask a clarifying question rather than guessing.
|
|
1501
|
+
- When listing items, provide the most relevant fields. Do not dump raw JSON unless the user asks for it.
|
|
1502
|
+
- For contract addresses, always use the full 0x-prefixed hex format.
|
|
1503
|
+
- When suggesting detection templates, explain why a particular template fits the user's needs.
|
|
1504
|
+
- If a user asks about something outside blockchain monitoring, politely redirect them.
|
|
1505
|
+
- When an operation fails, explain the error clearly and suggest how to fix it.
|
|
1506
|
+
- Use network IDs (e.g. "ethereum-mainnet") when calling tools, not chain names or slugs.`;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/ai/conversation.ts
|
|
1510
|
+
var MAX_MESSAGES = 50;
|
|
1511
|
+
var Conversation = class {
|
|
1512
|
+
messages = [];
|
|
1513
|
+
constructor(systemPrompt) {
|
|
1514
|
+
this.messages = [{ role: "system", content: systemPrompt }];
|
|
1515
|
+
}
|
|
1516
|
+
add(message) {
|
|
1517
|
+
this.messages.push(message);
|
|
1518
|
+
this.trim();
|
|
1519
|
+
}
|
|
1520
|
+
getMessages() {
|
|
1521
|
+
return this.messages;
|
|
1522
|
+
}
|
|
1523
|
+
getMessageCount() {
|
|
1524
|
+
return this.messages.length - 1;
|
|
1525
|
+
}
|
|
1526
|
+
clear() {
|
|
1527
|
+
const system = this.messages[0];
|
|
1528
|
+
this.messages = [system];
|
|
1529
|
+
}
|
|
1530
|
+
trim() {
|
|
1531
|
+
if (this.messages.length > MAX_MESSAGES + 1) {
|
|
1532
|
+
const system = this.messages[0];
|
|
1533
|
+
this.messages = [system, ...this.messages.slice(-MAX_MESSAGES)];
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
// src/repl/repl.ts
|
|
1539
|
+
async function startRepl() {
|
|
1540
|
+
if (!getApiKey()) {
|
|
1541
|
+
printError("Not authenticated. Run: chainalert login");
|
|
1542
|
+
process.exit(1);
|
|
1543
|
+
}
|
|
1544
|
+
const openai = createOpenAIClient();
|
|
1545
|
+
const conversation = new Conversation(buildSystemPrompt());
|
|
1546
|
+
print(bold("ChainAlert Interactive Mode"));
|
|
1547
|
+
print(
|
|
1548
|
+
dim(
|
|
1549
|
+
"Type your questions in natural language. Use /help for commands, /quit to exit.\n"
|
|
1550
|
+
)
|
|
1551
|
+
);
|
|
1552
|
+
const rl = createInterface({
|
|
1553
|
+
input: process.stdin,
|
|
1554
|
+
output: process.stdout,
|
|
1555
|
+
prompt: cyan("chainalert> ")
|
|
1556
|
+
});
|
|
1557
|
+
rl.prompt();
|
|
1558
|
+
rl.on("line", async (line) => {
|
|
1559
|
+
const input = line.trim();
|
|
1560
|
+
if (!input) {
|
|
1561
|
+
rl.prompt();
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
if (input === "/quit" || input === "/exit") {
|
|
1565
|
+
rl.close();
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (input === "/clear") {
|
|
1569
|
+
conversation.clear();
|
|
1570
|
+
print("Conversation cleared.");
|
|
1571
|
+
rl.prompt();
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (input === "/help") {
|
|
1575
|
+
printHelp();
|
|
1576
|
+
rl.prompt();
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (input === "/history") {
|
|
1580
|
+
print(
|
|
1581
|
+
`Conversation has ${conversation.getMessageCount()} messages (excluding system prompt).`
|
|
1582
|
+
);
|
|
1583
|
+
rl.prompt();
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
conversation.add({ role: "user", content: input });
|
|
1587
|
+
try {
|
|
1588
|
+
let done = false;
|
|
1589
|
+
while (!done) {
|
|
1590
|
+
const response = await openai.chat.completions.create({
|
|
1591
|
+
model: "gpt-4o",
|
|
1592
|
+
messages: conversation.getMessages(),
|
|
1593
|
+
tools,
|
|
1594
|
+
tool_choice: "auto"
|
|
1595
|
+
});
|
|
1596
|
+
const choice = response.choices[0];
|
|
1597
|
+
const message = choice.message;
|
|
1598
|
+
conversation.add(message);
|
|
1599
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
1600
|
+
for (const toolCall of message.tool_calls) {
|
|
1601
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
1602
|
+
printDim(` [calling ${toolCall.function.name}...]`);
|
|
1603
|
+
try {
|
|
1604
|
+
const result = await executeTool(toolCall.function.name, args);
|
|
1605
|
+
conversation.add({
|
|
1606
|
+
role: "tool",
|
|
1607
|
+
tool_call_id: toolCall.id,
|
|
1608
|
+
content: JSON.stringify(result)
|
|
1609
|
+
});
|
|
1610
|
+
} catch (err) {
|
|
1611
|
+
const errorMsg = err.message ?? "Tool execution failed";
|
|
1612
|
+
printError(errorMsg);
|
|
1613
|
+
conversation.add({
|
|
1614
|
+
role: "tool",
|
|
1615
|
+
tool_call_id: toolCall.id,
|
|
1616
|
+
content: JSON.stringify({ error: errorMsg })
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
} else {
|
|
1621
|
+
if (message.content) {
|
|
1622
|
+
print("\n" + message.content + "\n");
|
|
1623
|
+
}
|
|
1624
|
+
done = true;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
} catch (err) {
|
|
1628
|
+
printError(err.message ?? "AI request failed");
|
|
1629
|
+
}
|
|
1630
|
+
rl.prompt();
|
|
1631
|
+
});
|
|
1632
|
+
rl.on("close", () => {
|
|
1633
|
+
print("\nGoodbye!");
|
|
1634
|
+
process.exit(0);
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
function printHelp() {
|
|
1638
|
+
print(bold("\nSlash Commands:"));
|
|
1639
|
+
print(" /help Show this help message");
|
|
1640
|
+
print(" /clear Clear conversation history");
|
|
1641
|
+
print(" /history Show conversation message count");
|
|
1642
|
+
print(" /quit Exit interactive mode");
|
|
1643
|
+
print("");
|
|
1644
|
+
print(bold("Examples:"));
|
|
1645
|
+
print(' "Show me my active detections"');
|
|
1646
|
+
print(
|
|
1647
|
+
' "Create a large transfer detection on Ethereum for USDC with threshold 1000000"'
|
|
1648
|
+
);
|
|
1649
|
+
print(' "What detection templates are available?"');
|
|
1650
|
+
print(' "Show my alert stats"');
|
|
1651
|
+
print(' "Set up a Slack channel for notifications"');
|
|
1652
|
+
print(' "Pause detection det_abc123"');
|
|
1653
|
+
print("");
|
|
1654
|
+
}
|
|
1655
|
+
export {
|
|
1656
|
+
startRepl
|
|
1657
|
+
};
|