hive-intelligence 1.1.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +315 -85
- package/agent-skills/.claude-plugin/plugin.json +10 -4
- package/agent-skills/.mcp.json +8 -0
- package/agent-skills/AGENTS.md +37 -0
- package/agent-skills/README.md +28 -23
- package/agent-skills/SKILL.md +40 -10
- package/agent-skills/evals/trigger-queries.json +270 -0
- package/agent-skills/package.json +4 -4
- package/agent-skills/packs/hive-build/SKILL.md +54 -25
- package/agent-skills/packs/hive-build/evals/evals.json +16 -3
- package/agent-skills/packs/hive-build/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-build-onboarding/SKILL.md +27 -34
- package/agent-skills/packs/hive-build-onboarding/evals/evals.json +1 -1
- package/agent-skills/packs/hive-build-onboarding/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-cli/SKILL.md +33 -35
- package/agent-skills/packs/hive-cli/evals/evals.json +1 -1
- package/agent-skills/packs/hive-cli/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-defi-research/SKILL.md +105 -19
- package/agent-skills/packs/hive-defi-research/evals/evals.json +6 -3
- package/agent-skills/packs/hive-defi-research/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-dex-pool-analysis/SKILL.md +106 -16
- package/agent-skills/packs/hive-dex-pool-analysis/evals/evals.json +5 -3
- package/agent-skills/packs/hive-dex-pool-analysis/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-market-research/SKILL.md +130 -21
- package/agent-skills/packs/hive-market-research/evals/evals.json +15 -2
- package/agent-skills/packs/hive-market-research/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-mcp/SKILL.md +98 -153
- package/agent-skills/packs/hive-mcp/evals/evals.json +4 -3
- package/agent-skills/packs/hive-mcp/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-mcp/references/client-install-matrix.md +62 -35
- package/agent-skills/packs/hive-mcp/references/clients.md +154 -0
- package/agent-skills/packs/hive-network-infrastructure/SKILL.md +105 -20
- package/agent-skills/packs/hive-network-infrastructure/evals/evals.json +4 -2
- package/agent-skills/packs/hive-network-infrastructure/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-nft-research/SKILL.md +105 -18
- package/agent-skills/packs/hive-nft-research/evals/evals.json +4 -2
- package/agent-skills/packs/hive-nft-research/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-prediction-markets/SKILL.md +108 -21
- package/agent-skills/packs/hive-prediction-markets/evals/evals.json +4 -2
- package/agent-skills/packs/hive-prediction-markets/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-query/SKILL.md +80 -33
- package/agent-skills/packs/hive-query/evals/evals.json +7 -5
- package/agent-skills/packs/hive-query/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-query/references/root-mcp-workflow.md +39 -18
- package/agent-skills/packs/hive-security-risk/SKILL.md +110 -19
- package/agent-skills/packs/hive-security-risk/evals/evals.json +4 -2
- package/agent-skills/packs/hive-security-risk/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-solana-analysis/SKILL.md +115 -19
- package/agent-skills/packs/hive-solana-analysis/evals/evals.json +4 -2
- package/agent-skills/packs/hive-solana-analysis/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-stateful-monitoring/SKILL.md +120 -0
- package/agent-skills/packs/hive-stateful-monitoring/evals/evals.json +54 -0
- package/agent-skills/packs/hive-stateful-monitoring/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-stateful-monitoring/references/b2b-subject-context.md +53 -0
- package/agent-skills/packs/hive-token-diligence/SKILL.md +119 -21
- package/agent-skills/packs/hive-token-diligence/evals/evals.json +6 -4
- package/agent-skills/packs/hive-token-diligence/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-tool-discovery/SKILL.md +48 -19
- package/agent-skills/packs/hive-tool-discovery/evals/evals.json +17 -3
- package/agent-skills/packs/hive-tool-discovery/evals/triggers.json +50 -0
- package/agent-skills/packs/hive-tool-discovery/references/discovery-surfaces.md +31 -6
- package/agent-skills/packs/hive-wallet-investigation/SKILL.md +111 -18
- package/agent-skills/packs/hive-wallet-investigation/evals/evals.json +5 -3
- package/agent-skills/packs/hive-wallet-investigation/evals/triggers.json +50 -0
- package/agent-skills/references/agents-routing.md +19 -7
- package/agent-skills/scripts/validate-agent-skills-package.mjs +116 -18
- package/agent-skills/skills.sh.json +3 -2
- package/bin/hive-mcp.js +5 -5
- package/build/{alias-CZ3DEYHF.js → alias-BKAVOYQV.js} +2 -3
- package/build/{api-client-I23XLSHU.js → api-client-FTVDFW5V.js} +4 -4
- package/build/{auth-KKINNBXT.js → auth-EZFJ5RYB.js} +3 -4
- package/build/{browser-auth-EEFGRHWY.js → browser-auth-OJSUNFBI.js} +2 -3
- package/build/{cache-EGNMENNA.js → cache-TBDQ7QBU.js} +2 -3
- package/build/{chunk-V77LO4RO.js → chunk-6PC3HUAK.js} +10 -4
- package/build/{chunk-XYBEFFZ3.js → chunk-725R3KLJ.js} +26 -4
- package/build/{chunk-PGBVNMNZ.js → chunk-7GD2MFYI.js} +57 -7
- package/build/{chunk-IDZD2NVG.js → chunk-AEU43ACS.js} +33 -10
- package/build/chunk-EPF36Q3Z.js +71 -0
- package/build/chunk-I5Z6FZRL.js +178 -0
- package/build/{chunk-KBVPIZS4.js → chunk-ILVPOLA7.js} +12 -4
- package/build/{chunk-B6G222LS.js → chunk-L326MQZP.js} +22 -6
- package/build/{chunk-QNCI4DEB.js → chunk-M6REON2I.js} +35 -21
- package/build/chunk-N32UIHBU.js +125 -0
- package/build/{chunk-IZTN7WFH.js → chunk-Q7VBYLX7.js} +12 -3
- package/build/chunk-QDZ7MAVN.js +447 -0
- package/build/{chunk-KGYBTXIN.js → chunk-R6YZTFQL.js} +2 -1
- package/build/{chunk-DYJBTKNR.js → chunk-RU6QOFHH.js} +9 -10
- package/build/{chunk-RKICZ6DZ.js → chunk-RUHO24SX.js} +16 -10
- package/build/{chunk-MPIUUE3T.js → chunk-TNQ7GJ6Q.js} +0 -60
- package/build/{chunk-2X3U4DFK.js → chunk-W2QL7LYI.js} +43586 -21498
- package/build/chunk-ZXKFJQDE.js +20 -0
- package/build/cli.js +3342 -3410
- package/build/{completion-BELZL7ND.js → completion-7KBW243H.js} +4 -5
- package/build/{config-TDX4VUGW.js → config-KAEUX6RM.js} +23 -4
- package/build/{config-dir-V43W2WIA.js → config-dir-5IH7MOOT.js} +2 -3
- package/build/{doctor-5K5UWHNX.js → doctor-ORDOCGLM.js} +34 -10
- package/build/{errors-E6MVUBNA.js → errors-HGHUF575.js} +2 -3
- package/build/{format-TSFSI2K7.js → format-H7FOQSYY.js} +1 -2
- package/build/{init-YQ5NBICF.js → init-UCTCUYFA.js} +52 -9
- package/build/init-all-BVWW3JVK.js +408 -0
- package/build/mcpServer-RUEOTTF7.js +34 -0
- package/build/monitor-worker.js +1396 -0
- package/build/{namespace-PVHBFDW2.js → namespace-L5DGCY7K.js} +2 -3
- package/build/{open-YUIJP37F.js → open-V2RNA3QT.js} +2 -3
- package/build/{output-IYJBFCYK.js → output-ITGLBKIG.js} +2 -3
- package/build/release.json +8 -0
- package/build/{serve-KWNJK6EY.js → serve-DKLR2WQH.js} +9 -4
- package/build/server.js +5399 -2945
- package/build/{setup-YTUKMPNP.js → setup-PTYFMTK5.js} +60 -56
- package/build/stdio.js +10 -8
- package/build/{telemetry-5IPRKZBL.js → telemetry-HPLDYR7U.js} +5 -4
- package/build/{tools-PJHWFCAG.js → tools-I7GLWTV6.js} +15 -15
- package/build/{uninstall-HWXEIP6K.js → uninstall-SAOIPL6K.js} +19 -11
- package/build/update-check-IEXPOYYR.js +18 -0
- package/build/upgrade-J4HIGCKE.js +171 -0
- package/build/version-Z4HJL4VV.js +16 -0
- package/build/{watch-KZ5EDZK3.js → watch-WVR4CGCD.js} +8 -7
- package/package.json +82 -41
- package/agent-skills/packs/hive-defi-research/references/workflow.md +0 -41
- package/agent-skills/packs/hive-dex-pool-analysis/references/workflow.md +0 -42
- package/agent-skills/packs/hive-market-research/references/workflow.md +0 -47
- package/agent-skills/packs/hive-network-infrastructure/references/workflow.md +0 -41
- package/agent-skills/packs/hive-nft-research/references/workflow.md +0 -42
- package/agent-skills/packs/hive-prediction-markets/references/workflow.md +0 -42
- package/agent-skills/packs/hive-security-risk/references/workflow.md +0 -46
- package/agent-skills/packs/hive-solana-analysis/references/workflow.md +0 -42
- package/agent-skills/packs/hive-token-diligence/references/workflow.md +0 -45
- package/agent-skills/packs/hive-wallet-investigation/references/workflow.md +0 -45
- package/build/alias-CZ3DEYHF.js.map +0 -1
- package/build/api-client-I23XLSHU.js.map +0 -1
- package/build/auth-KKINNBXT.js.map +0 -1
- package/build/browser-auth-EEFGRHWY.js.map +0 -1
- package/build/cache-EGNMENNA.js.map +0 -1
- package/build/chunk-2MYMHMBF.js +0 -340
- package/build/chunk-2MYMHMBF.js.map +0 -1
- package/build/chunk-2X3U4DFK.js.map +0 -1
- package/build/chunk-B6G222LS.js.map +0 -1
- package/build/chunk-DYJBTKNR.js.map +0 -1
- package/build/chunk-IDZD2NVG.js.map +0 -1
- package/build/chunk-IZTN7WFH.js.map +0 -1
- package/build/chunk-KBVPIZS4.js.map +0 -1
- package/build/chunk-KGYBTXIN.js.map +0 -1
- package/build/chunk-MPIUUE3T.js.map +0 -1
- package/build/chunk-PGBVNMNZ.js.map +0 -1
- package/build/chunk-QNCI4DEB.js.map +0 -1
- package/build/chunk-RKICZ6DZ.js.map +0 -1
- package/build/chunk-UKHZIVDT.js +0 -44
- package/build/chunk-UKHZIVDT.js.map +0 -1
- package/build/chunk-V77LO4RO.js.map +0 -1
- package/build/chunk-XYBEFFZ3.js.map +0 -1
- package/build/cli.js.map +0 -1
- package/build/completion-BELZL7ND.js.map +0 -1
- package/build/config-TDX4VUGW.js.map +0 -1
- package/build/config-dir-V43W2WIA.js.map +0 -1
- package/build/doctor-5K5UWHNX.js.map +0 -1
- package/build/errors-E6MVUBNA.js.map +0 -1
- package/build/format-TSFSI2K7.js.map +0 -1
- package/build/init-YQ5NBICF.js.map +0 -1
- package/build/init-all-6OWEQALB.js +0 -274
- package/build/init-all-6OWEQALB.js.map +0 -1
- package/build/mcpServer-AEX3RQ4U.js +0 -20
- package/build/mcpServer-AEX3RQ4U.js.map +0 -1
- package/build/namespace-PVHBFDW2.js.map +0 -1
- package/build/open-YUIJP37F.js.map +0 -1
- package/build/output-IYJBFCYK.js.map +0 -1
- package/build/serve-KWNJK6EY.js.map +0 -1
- package/build/server.js.map +0 -1
- package/build/setup-YTUKMPNP.js.map +0 -1
- package/build/stdio.js.map +0 -1
- package/build/telemetry-5IPRKZBL.js.map +0 -1
- package/build/tools-PJHWFCAG.js.map +0 -1
- package/build/uninstall-HWXEIP6K.js.map +0 -1
- package/build/watch-KZ5EDZK3.js.map +0 -1
|
@@ -0,0 +1,1396 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
consumeCredits,
|
|
4
|
+
executeToolByName
|
|
5
|
+
} from "./chunk-I5Z6FZRL.js";
|
|
6
|
+
import {
|
|
7
|
+
HyperliquidAPI,
|
|
8
|
+
MIN_MONITOR_CADENCE_MS,
|
|
9
|
+
closeRedisClient,
|
|
10
|
+
collectRwaRows,
|
|
11
|
+
getEnvVar,
|
|
12
|
+
getSupabaseAdmin,
|
|
13
|
+
isManualMonitorCadence,
|
|
14
|
+
isSupabaseConfigured,
|
|
15
|
+
monitorCadenceIntervalMs,
|
|
16
|
+
runScheduledHealthCanaries,
|
|
17
|
+
secureLogger
|
|
18
|
+
} from "./chunk-W2QL7LYI.js";
|
|
19
|
+
import "./chunk-L326MQZP.js";
|
|
20
|
+
import "./chunk-EPF36Q3Z.js";
|
|
21
|
+
import "./chunk-ZXKFJQDE.js";
|
|
22
|
+
import "./chunk-ILVPOLA7.js";
|
|
23
|
+
|
|
24
|
+
// src/monitor-worker.ts
|
|
25
|
+
import { resolve } from "path";
|
|
26
|
+
import { fileURLToPath } from "url";
|
|
27
|
+
|
|
28
|
+
// src/services/monitor-runner.ts
|
|
29
|
+
import { createHash } from "crypto";
|
|
30
|
+
var DEFAULT_BATCH_SIZE = 10;
|
|
31
|
+
var DEFAULT_LOCK_TIMEOUT_SECONDS = 300;
|
|
32
|
+
var DEFAULT_WORKER_ID = `hive-monitor-worker-${process.pid}`;
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function stringValue(value) {
|
|
37
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
38
|
+
}
|
|
39
|
+
function numberValue(value) {
|
|
40
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
41
|
+
if (typeof value === "string" && value.trim()) {
|
|
42
|
+
const parsed = Number(value);
|
|
43
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
44
|
+
}
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
function booleanValue(value) {
|
|
48
|
+
if (typeof value === "boolean") return value;
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
const normalized = value.trim().toLowerCase();
|
|
51
|
+
if (["true", "1", "yes"].includes(normalized)) return true;
|
|
52
|
+
if (["false", "0", "no"].includes(normalized)) return false;
|
|
53
|
+
}
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
function recordArray(value) {
|
|
57
|
+
if (!Array.isArray(value)) return [];
|
|
58
|
+
return value.slice(0, 5).map((item) => {
|
|
59
|
+
if (isRecord(item)) return item;
|
|
60
|
+
if (typeof item === "string" && item.trim()) return { id: item.trim() };
|
|
61
|
+
return null;
|
|
62
|
+
}).filter((item) => Boolean(item));
|
|
63
|
+
}
|
|
64
|
+
function stringArray(value) {
|
|
65
|
+
if (!Array.isArray(value)) return [];
|
|
66
|
+
return value.map((item) => typeof item === "string" ? item.trim() : void 0).filter((item) => Boolean(item)).slice(0, 5);
|
|
67
|
+
}
|
|
68
|
+
function normalizeAlchemyNetwork(target) {
|
|
69
|
+
const raw = stringValue(target.network) ?? stringValue(target.chain) ?? "ethereum";
|
|
70
|
+
const normalized = raw.toLowerCase();
|
|
71
|
+
const aliases = {
|
|
72
|
+
eth: "eth-mainnet",
|
|
73
|
+
ethereum: "eth-mainnet",
|
|
74
|
+
mainnet: "eth-mainnet",
|
|
75
|
+
base: "base-mainnet",
|
|
76
|
+
polygon: "polygon-mainnet",
|
|
77
|
+
matic: "polygon-mainnet",
|
|
78
|
+
arbitrum: "arb-mainnet",
|
|
79
|
+
arb: "arb-mainnet",
|
|
80
|
+
optimism: "opt-mainnet",
|
|
81
|
+
opt: "opt-mainnet",
|
|
82
|
+
zksync: "zksync-mainnet",
|
|
83
|
+
scroll: "scroll-mainnet",
|
|
84
|
+
linea: "linea-mainnet",
|
|
85
|
+
worldchain: "worldchain-mainnet"
|
|
86
|
+
};
|
|
87
|
+
return aliases[normalized] ?? raw;
|
|
88
|
+
}
|
|
89
|
+
function normalizeNetworkId(target) {
|
|
90
|
+
const explicit = numberValue(target.networkId) ?? numberValue(target.network_id) ?? numberValue(target.chainId) ?? numberValue(target.chain_id);
|
|
91
|
+
if (explicit) return explicit;
|
|
92
|
+
const raw = stringValue(target.network) ?? stringValue(target.chain) ?? "ethereum";
|
|
93
|
+
const aliases = {
|
|
94
|
+
eth: 1,
|
|
95
|
+
ethereum: 1,
|
|
96
|
+
mainnet: 1,
|
|
97
|
+
base: 8453,
|
|
98
|
+
polygon: 137,
|
|
99
|
+
matic: 137,
|
|
100
|
+
arbitrum: 42161,
|
|
101
|
+
arb: 42161,
|
|
102
|
+
optimism: 10,
|
|
103
|
+
opt: 10,
|
|
104
|
+
bsc: 56,
|
|
105
|
+
binance: 56,
|
|
106
|
+
avalanche: 43114,
|
|
107
|
+
avax: 43114,
|
|
108
|
+
linea: 59144,
|
|
109
|
+
scroll: 534352,
|
|
110
|
+
zksync: 324
|
|
111
|
+
};
|
|
112
|
+
return aliases[raw.toLowerCase()] ?? 1;
|
|
113
|
+
}
|
|
114
|
+
function isSolanaWalletTarget(target) {
|
|
115
|
+
const raw = stringValue(target.network) ?? stringValue(target.chain);
|
|
116
|
+
return ["sol", "solana", "svm", "solana-mainnet"].includes(
|
|
117
|
+
raw?.toLowerCase() ?? ""
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
function getMonitorTargetAddress(target) {
|
|
121
|
+
return stringValue(target.address) ?? stringValue(target.wallet) ?? stringValue(target.wallet_address) ?? stringValue(target.token_address) ?? stringValue(target.contract);
|
|
122
|
+
}
|
|
123
|
+
function buildWalletWorkflowPlan(monitor) {
|
|
124
|
+
const address = getMonitorTargetAddress(monitor.target);
|
|
125
|
+
if (!address) {
|
|
126
|
+
throw new Error("Wallet monitor target must include address.");
|
|
127
|
+
}
|
|
128
|
+
if (isSolanaWalletTarget(monitor.target)) {
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
toolName: "helius_get_wallet_balances",
|
|
132
|
+
snapshotKey: "wallet_balances",
|
|
133
|
+
args: {
|
|
134
|
+
wallet: address,
|
|
135
|
+
limit: 100,
|
|
136
|
+
showNative: true,
|
|
137
|
+
showNfts: false
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
toolName: "helius_get_wallet_transfers",
|
|
142
|
+
snapshotKey: "wallet_transfers",
|
|
143
|
+
args: {
|
|
144
|
+
wallet: address,
|
|
145
|
+
limit: 25
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
toolName: "helius_get_wallet_history",
|
|
150
|
+
snapshotKey: "wallet_history",
|
|
151
|
+
args: {
|
|
152
|
+
wallet: address,
|
|
153
|
+
limit: 25
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
const network = normalizeAlchemyNetwork(monitor.target);
|
|
159
|
+
const transferCategories = ["external", "erc20"];
|
|
160
|
+
const fromBlock = stringValue(monitor.target.from_block) ?? "0x0";
|
|
161
|
+
return [
|
|
162
|
+
{
|
|
163
|
+
toolName: "alchemy_get_token_balances_by_wallet",
|
|
164
|
+
snapshotKey: "token_balances",
|
|
165
|
+
args: {
|
|
166
|
+
address,
|
|
167
|
+
network,
|
|
168
|
+
includeNativeTokens: true,
|
|
169
|
+
includeErc20Tokens: true
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
toolName: "alchemy_get_native_balance",
|
|
174
|
+
snapshotKey: "native_balance",
|
|
175
|
+
args: { address, network }
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
toolName: "alchemy_get_asset_transfers",
|
|
179
|
+
snapshotKey: "outgoing_transfers",
|
|
180
|
+
args: {
|
|
181
|
+
network,
|
|
182
|
+
fromAddress: address,
|
|
183
|
+
fromBlock,
|
|
184
|
+
toBlock: "latest",
|
|
185
|
+
category: transferCategories,
|
|
186
|
+
maxCount: "0x5"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
toolName: "alchemy_get_asset_transfers",
|
|
191
|
+
snapshotKey: "incoming_transfers",
|
|
192
|
+
args: {
|
|
193
|
+
network,
|
|
194
|
+
toAddress: address,
|
|
195
|
+
fromBlock,
|
|
196
|
+
toBlock: "latest",
|
|
197
|
+
category: transferCategories,
|
|
198
|
+
maxCount: "0x5"
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
function buildTokenWorkflowPlan(monitor) {
|
|
204
|
+
const address = getMonitorTargetAddress(monitor.target);
|
|
205
|
+
if (!address) {
|
|
206
|
+
throw new Error("Token monitor target must include address.");
|
|
207
|
+
}
|
|
208
|
+
const network = normalizeAlchemyNetwork(monitor.target);
|
|
209
|
+
const networkId = normalizeNetworkId(monitor.target);
|
|
210
|
+
return [
|
|
211
|
+
{
|
|
212
|
+
toolName: "alchemy_get_token_metadata",
|
|
213
|
+
snapshotKey: "metadata",
|
|
214
|
+
args: { network, address }
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
toolName: "alchemy_get_token_prices_by_address",
|
|
218
|
+
snapshotKey: "alchemy_price",
|
|
219
|
+
args: { network, address }
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
toolName: "get_token_security",
|
|
223
|
+
snapshotKey: "security",
|
|
224
|
+
args: {
|
|
225
|
+
chainId: String(networkId),
|
|
226
|
+
contract_addresses: address
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
toolName: "codex_get_token_price",
|
|
231
|
+
snapshotKey: "dex_price",
|
|
232
|
+
args: { inputs: [{ networkId, address }] }
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
toolName: "get_token_holders",
|
|
236
|
+
snapshotKey: "holders",
|
|
237
|
+
args: { networkId, address }
|
|
238
|
+
}
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
function buildProtocolWorkflowPlan(monitor) {
|
|
242
|
+
const protocol = stringValue(monitor.target.protocol) ?? stringValue(monitor.target.slug) ?? stringValue(monitor.target.name);
|
|
243
|
+
if (!protocol) {
|
|
244
|
+
throw new Error("Protocol monitor target must include protocol.");
|
|
245
|
+
}
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
toolName: "get_protocol_tvl",
|
|
249
|
+
snapshotKey: "tvl",
|
|
250
|
+
args: { protocol }
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
toolName: "get_protocol_fees",
|
|
254
|
+
snapshotKey: "fees",
|
|
255
|
+
args: { protocol }
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
toolName: "get_protocol_fee_summary",
|
|
259
|
+
snapshotKey: "fee_summary",
|
|
260
|
+
args: { protocol }
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
toolName: "get_global_defi",
|
|
264
|
+
snapshotKey: "global_defi",
|
|
265
|
+
args: {}
|
|
266
|
+
}
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
function buildMarketWorkflowPlan(monitor) {
|
|
270
|
+
const ids = stringValue(monitor.target.ids) ?? stringValue(monitor.target.id) ?? stringValue(monitor.target.coin_id) ?? stringValue(monitor.target.symbol) ?? stringValue(monitor.target.name);
|
|
271
|
+
if (!ids) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
"Market monitor target must include ids, id, coin_id, symbol, or name."
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const vsCurrencies = stringValue(monitor.target.vs_currencies) ?? stringValue(monitor.target.vs_currency) ?? "usd";
|
|
277
|
+
const calls = [
|
|
278
|
+
{
|
|
279
|
+
toolName: "get_price",
|
|
280
|
+
snapshotKey: "price",
|
|
281
|
+
args: {
|
|
282
|
+
ids,
|
|
283
|
+
vs_currencies: vsCurrencies,
|
|
284
|
+
include_market_cap: true,
|
|
285
|
+
include_24hr_vol: true,
|
|
286
|
+
include_24hr_change: true,
|
|
287
|
+
include_last_updated_at: true
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
];
|
|
291
|
+
if (booleanValue(monitor.target.include_trending_pools) === true) {
|
|
292
|
+
calls.push({
|
|
293
|
+
toolName: "get_trending_pools",
|
|
294
|
+
snapshotKey: "trending_pools",
|
|
295
|
+
args: {
|
|
296
|
+
duration: stringValue(monitor.target.trending_duration) ?? "24h",
|
|
297
|
+
include: "base_token,quote_token,dex,network",
|
|
298
|
+
page: numberValue(monitor.target.trending_page) ?? 1
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return calls;
|
|
303
|
+
}
|
|
304
|
+
function buildPredictionMarketWorkflowPlan(monitor) {
|
|
305
|
+
const marketId = stringValue(monitor.target.marketId) ?? stringValue(monitor.target.market_id) ?? stringValue(monitor.target.id);
|
|
306
|
+
if (marketId) {
|
|
307
|
+
return [
|
|
308
|
+
{
|
|
309
|
+
toolName: "codex_prediction_market_price",
|
|
310
|
+
snapshotKey: "market_price",
|
|
311
|
+
args: { marketId }
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
toolName: "codex_prediction_market_stats",
|
|
315
|
+
snapshotKey: "market_stats",
|
|
316
|
+
args: {
|
|
317
|
+
marketId,
|
|
318
|
+
bucketCount: numberValue(monitor.target.bucket_count) ?? 6,
|
|
319
|
+
durations: ["hour1", "day1"]
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
];
|
|
323
|
+
}
|
|
324
|
+
const phrase = stringValue(monitor.target.phrase) ?? stringValue(monitor.target.query) ?? stringValue(monitor.target.title);
|
|
325
|
+
if (!phrase) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
"Prediction-market monitor target must include marketId or phrase."
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return [
|
|
331
|
+
{
|
|
332
|
+
toolName: "codex_filter_prediction_markets",
|
|
333
|
+
snapshotKey: "market_search",
|
|
334
|
+
args: {
|
|
335
|
+
phrase,
|
|
336
|
+
limit: numberValue(monitor.target.limit) ?? 10,
|
|
337
|
+
offset: numberValue(monitor.target.offset) ?? 0
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
function buildWatchlistDigestWorkflowPlan(monitor) {
|
|
343
|
+
const calls = [];
|
|
344
|
+
recordArray(monitor.target.wallets).slice(0, 2).forEach((wallet, index) => {
|
|
345
|
+
const target = { ...wallet };
|
|
346
|
+
if (!getMonitorTargetAddress(target) && typeof wallet.id === "string") {
|
|
347
|
+
target.address = wallet.id;
|
|
348
|
+
}
|
|
349
|
+
buildWalletWorkflowPlan({ target }).slice(0, 2).forEach((call) => {
|
|
350
|
+
calls.push({
|
|
351
|
+
...call,
|
|
352
|
+
snapshotKey: `wallet_${index}_${call.snapshotKey}`
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
recordArray(monitor.target.tokens).slice(0, 2).forEach((token, index) => {
|
|
357
|
+
const target = { ...token };
|
|
358
|
+
if (!getMonitorTargetAddress(target) && typeof token.id === "string") {
|
|
359
|
+
target.address = token.id;
|
|
360
|
+
}
|
|
361
|
+
buildTokenWorkflowPlan({ target }).slice(0, 3).forEach((call) => {
|
|
362
|
+
calls.push({
|
|
363
|
+
...call,
|
|
364
|
+
snapshotKey: `token_${index}_${call.snapshotKey}`
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
recordArray(monitor.target.protocols).slice(0, 2).forEach((protocol, index) => {
|
|
369
|
+
const target = { ...protocol };
|
|
370
|
+
if (!stringValue(target.protocol) && typeof protocol.id === "string") {
|
|
371
|
+
target.protocol = protocol.id;
|
|
372
|
+
}
|
|
373
|
+
buildProtocolWorkflowPlan({ target }).slice(0, 2).forEach((call) => {
|
|
374
|
+
calls.push({
|
|
375
|
+
...call,
|
|
376
|
+
snapshotKey: `protocol_${index}_${call.snapshotKey}`
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
recordArray(monitor.target.markets).slice(0, 2).forEach((market, index) => {
|
|
381
|
+
const target = { ...market };
|
|
382
|
+
if (!stringValue(target.ids) && typeof market.id === "string") {
|
|
383
|
+
target.ids = market.id;
|
|
384
|
+
}
|
|
385
|
+
buildMarketWorkflowPlan({ target }).slice(0, 1).forEach((call) => {
|
|
386
|
+
calls.push({
|
|
387
|
+
...call,
|
|
388
|
+
snapshotKey: `market_${index}_${call.snapshotKey}`
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
recordArray(monitor.target.prediction_markets).slice(0, 2).forEach((predictionMarket, index) => {
|
|
393
|
+
const target = { ...predictionMarket };
|
|
394
|
+
if (!stringValue(target.marketId) && typeof predictionMarket.id === "string") {
|
|
395
|
+
target.marketId = predictionMarket.id;
|
|
396
|
+
}
|
|
397
|
+
buildPredictionMarketWorkflowPlan({ target }).slice(0, 1).forEach((call) => {
|
|
398
|
+
calls.push({
|
|
399
|
+
...call,
|
|
400
|
+
snapshotKey: `prediction_market_${index}_${call.snapshotKey}`
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
if (calls.length === 0) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
"Watchlist digest target must include at least one wallet, token, protocol, market, or prediction_market."
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return calls.slice(0, 20);
|
|
410
|
+
}
|
|
411
|
+
function buildTokenDiscoveryRiskWorkflowPlan(monitor) {
|
|
412
|
+
const calls = [];
|
|
413
|
+
const networks = stringArray(monitor.target.networks);
|
|
414
|
+
const include = "base_token,quote_token,dex,network";
|
|
415
|
+
if (networks.length === 0) {
|
|
416
|
+
calls.push({
|
|
417
|
+
toolName: "get_new_pools",
|
|
418
|
+
snapshotKey: "new_pools",
|
|
419
|
+
args: { include, page: numberValue(monitor.target.page) ?? 1 }
|
|
420
|
+
});
|
|
421
|
+
} else {
|
|
422
|
+
networks.slice(0, 3).forEach((network, index) => {
|
|
423
|
+
calls.push({
|
|
424
|
+
toolName: "get_new_pools_by_network",
|
|
425
|
+
snapshotKey: `new_pools_${index}`,
|
|
426
|
+
args: { network, include: "base_token,quote_token,dex", page: 1 }
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
calls.push({
|
|
431
|
+
toolName: "get_new_coins",
|
|
432
|
+
snapshotKey: "new_coins",
|
|
433
|
+
args: {}
|
|
434
|
+
});
|
|
435
|
+
stringArray(monitor.target.contracts ?? monitor.target.addresses).slice(0, 3).forEach((address, index) => {
|
|
436
|
+
const chainId = String(normalizeNetworkId(monitor.target));
|
|
437
|
+
calls.push({
|
|
438
|
+
toolName: "get_token_security",
|
|
439
|
+
snapshotKey: `token_security_${index}`,
|
|
440
|
+
args: { chainId, contract_addresses: address }
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
return calls;
|
|
444
|
+
}
|
|
445
|
+
function buildRiskWatchWorkflowPlan(monitor) {
|
|
446
|
+
const chainId = String(normalizeNetworkId(monitor.target));
|
|
447
|
+
const calls = [];
|
|
448
|
+
stringArray(monitor.target.addresses).slice(0, 5).forEach((address, index) => {
|
|
449
|
+
calls.push({
|
|
450
|
+
toolName: "check_malicious_address",
|
|
451
|
+
snapshotKey: `address_risk_${index}`,
|
|
452
|
+
args: { chainId, address }
|
|
453
|
+
});
|
|
454
|
+
calls.push({
|
|
455
|
+
toolName: "get_wallet_approvals",
|
|
456
|
+
snapshotKey: `wallet_approvals_${index}`,
|
|
457
|
+
args: { chainId, address }
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
stringArray(monitor.target.contracts).slice(0, 5).forEach((address, index) => {
|
|
461
|
+
calls.push({
|
|
462
|
+
toolName: "get_token_security",
|
|
463
|
+
snapshotKey: `contract_security_${index}`,
|
|
464
|
+
args: { chainId, contract_addresses: address }
|
|
465
|
+
});
|
|
466
|
+
calls.push({
|
|
467
|
+
toolName: "detect_rugpull",
|
|
468
|
+
snapshotKey: `rugpull_${index}`,
|
|
469
|
+
args: { chain_id: chainId, contract_address: address }
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
if (calls.length === 0) {
|
|
473
|
+
throw new Error("Risk-watch target must include addresses or contracts.");
|
|
474
|
+
}
|
|
475
|
+
return calls.slice(0, 20);
|
|
476
|
+
}
|
|
477
|
+
function buildMonitorWorkflowDefinition(monitor) {
|
|
478
|
+
switch (monitor.kind) {
|
|
479
|
+
case "wallet":
|
|
480
|
+
return {
|
|
481
|
+
type: "wallet",
|
|
482
|
+
label: "Wallet",
|
|
483
|
+
target: {
|
|
484
|
+
...monitor.target,
|
|
485
|
+
address: getMonitorTargetAddress(monitor.target),
|
|
486
|
+
network: isSolanaWalletTarget(monitor.target) ? "solana" : normalizeAlchemyNetwork(monitor.target)
|
|
487
|
+
},
|
|
488
|
+
calls: buildWalletWorkflowPlan(monitor)
|
|
489
|
+
};
|
|
490
|
+
case "token":
|
|
491
|
+
return {
|
|
492
|
+
type: "token",
|
|
493
|
+
label: "Token",
|
|
494
|
+
target: {
|
|
495
|
+
...monitor.target,
|
|
496
|
+
address: getMonitorTargetAddress(monitor.target),
|
|
497
|
+
network: normalizeAlchemyNetwork(monitor.target),
|
|
498
|
+
networkId: normalizeNetworkId(monitor.target)
|
|
499
|
+
},
|
|
500
|
+
calls: buildTokenWorkflowPlan(monitor)
|
|
501
|
+
};
|
|
502
|
+
case "protocol": {
|
|
503
|
+
const protocol = stringValue(monitor.target.protocol) ?? stringValue(monitor.target.slug) ?? stringValue(monitor.target.name);
|
|
504
|
+
return {
|
|
505
|
+
type: "protocol",
|
|
506
|
+
label: "Protocol",
|
|
507
|
+
target: { ...monitor.target, protocol },
|
|
508
|
+
calls: buildProtocolWorkflowPlan(monitor)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
case "market": {
|
|
512
|
+
const ids = stringValue(monitor.target.ids) ?? stringValue(monitor.target.id) ?? stringValue(monitor.target.coin_id) ?? stringValue(monitor.target.symbol) ?? stringValue(monitor.target.name);
|
|
513
|
+
return {
|
|
514
|
+
type: "market",
|
|
515
|
+
label: "Market",
|
|
516
|
+
target: {
|
|
517
|
+
...monitor.target,
|
|
518
|
+
ids,
|
|
519
|
+
vs_currencies: stringValue(monitor.target.vs_currencies) ?? stringValue(monitor.target.vs_currency) ?? "usd"
|
|
520
|
+
},
|
|
521
|
+
calls: buildMarketWorkflowPlan(monitor)
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
case "prediction_market":
|
|
525
|
+
return {
|
|
526
|
+
type: "prediction_market",
|
|
527
|
+
label: "Prediction market",
|
|
528
|
+
target: monitor.target,
|
|
529
|
+
calls: buildPredictionMarketWorkflowPlan(monitor)
|
|
530
|
+
};
|
|
531
|
+
case "watchlist_digest":
|
|
532
|
+
return {
|
|
533
|
+
type: "watchlist_digest",
|
|
534
|
+
label: "Watchlist digest",
|
|
535
|
+
target: monitor.target,
|
|
536
|
+
calls: buildWatchlistDigestWorkflowPlan(monitor)
|
|
537
|
+
};
|
|
538
|
+
case "token_discovery_risk":
|
|
539
|
+
return {
|
|
540
|
+
type: "token_discovery_risk",
|
|
541
|
+
label: "Token discovery risk",
|
|
542
|
+
target: monitor.target,
|
|
543
|
+
calls: buildTokenDiscoveryRiskWorkflowPlan(monitor)
|
|
544
|
+
};
|
|
545
|
+
case "risk_watch":
|
|
546
|
+
return {
|
|
547
|
+
type: "risk_watch",
|
|
548
|
+
label: "Risk watch",
|
|
549
|
+
target: monitor.target,
|
|
550
|
+
calls: buildRiskWatchWorkflowPlan(monitor)
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function sortedJson(value) {
|
|
555
|
+
if (Array.isArray(value)) return `[${value.map(sortedJson).join(",")}]`;
|
|
556
|
+
if (value && typeof value === "object") {
|
|
557
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${sortedJson(item)}`).join(",")}}`;
|
|
558
|
+
}
|
|
559
|
+
return JSON.stringify(value);
|
|
560
|
+
}
|
|
561
|
+
function stripVolatileFields(value) {
|
|
562
|
+
if (Array.isArray(value)) return value.map(stripVolatileFields);
|
|
563
|
+
if (!isRecord(value)) return value;
|
|
564
|
+
const omitted = /* @__PURE__ */ new Set([
|
|
565
|
+
"_hive",
|
|
566
|
+
"duration_ms",
|
|
567
|
+
"fetched_at",
|
|
568
|
+
"captured_at",
|
|
569
|
+
"created_at",
|
|
570
|
+
"updated_at",
|
|
571
|
+
"cache_status",
|
|
572
|
+
"request_id"
|
|
573
|
+
]);
|
|
574
|
+
return Object.fromEntries(
|
|
575
|
+
Object.entries(value).filter(([key]) => !omitted.has(key)).map(([key, item]) => [key, stripVolatileFields(item)])
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
function compactForStorage(value, depth = 0) {
|
|
579
|
+
if (depth > 8) return "[max-depth]";
|
|
580
|
+
if (typeof value === "string") {
|
|
581
|
+
return value.length > 5e3 ? `${value.slice(0, 5e3)}...[truncated]` : value;
|
|
582
|
+
}
|
|
583
|
+
if (Array.isArray(value)) {
|
|
584
|
+
const items = value.slice(0, 50).map((item) => compactForStorage(item, depth + 1));
|
|
585
|
+
if (value.length > items.length) {
|
|
586
|
+
items.push({ _truncated_items: value.length - items.length });
|
|
587
|
+
}
|
|
588
|
+
return items;
|
|
589
|
+
}
|
|
590
|
+
if (isRecord(value)) {
|
|
591
|
+
return Object.fromEntries(
|
|
592
|
+
Object.entries(value).map(([key, item]) => [
|
|
593
|
+
key,
|
|
594
|
+
compactForStorage(item, depth + 1)
|
|
595
|
+
])
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return value;
|
|
599
|
+
}
|
|
600
|
+
function fingerprintSnapshot(snapshot) {
|
|
601
|
+
return createHash("sha256").update(sortedJson(stripVolatileFields(snapshot))).digest("hex");
|
|
602
|
+
}
|
|
603
|
+
function computeNextRunAt(cadence, from = /* @__PURE__ */ new Date()) {
|
|
604
|
+
const normalized = (cadence ?? "manual").trim().toLowerCase();
|
|
605
|
+
if (isManualMonitorCadence(normalized)) return null;
|
|
606
|
+
const intervalMs = monitorCadenceIntervalMs(normalized);
|
|
607
|
+
if (intervalMs === null) return null;
|
|
608
|
+
return new Date(
|
|
609
|
+
from.getTime() + Math.max(intervalMs, MIN_MONITOR_CADENCE_MS)
|
|
610
|
+
).toISOString();
|
|
611
|
+
}
|
|
612
|
+
async function defaultConsumeCredit(args) {
|
|
613
|
+
return consumeCredits({
|
|
614
|
+
userId: args.userId,
|
|
615
|
+
cost: 1,
|
|
616
|
+
idempotencyKey: `monitor-run:${args.runId}`,
|
|
617
|
+
requestId: args.runId
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
async function debitMonitorRunCredit(args) {
|
|
621
|
+
try {
|
|
622
|
+
const debit = await args.consumeCredit({
|
|
623
|
+
userId: args.monitor.user_id,
|
|
624
|
+
runId: args.runId
|
|
625
|
+
});
|
|
626
|
+
if (debit.code === "insufficient" || debit.code === "no_wallet") {
|
|
627
|
+
return {
|
|
628
|
+
ok: false,
|
|
629
|
+
code: "MONITOR_CREDIT_EXHAUSTED",
|
|
630
|
+
message: "Monitor run skipped because the account has no available credits."
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return { ok: true };
|
|
634
|
+
} catch (error) {
|
|
635
|
+
secureLogger.warn("Monitor credit debit failed", {
|
|
636
|
+
monitorId: args.monitor.id,
|
|
637
|
+
error: error instanceof Error ? error.message : String(error)
|
|
638
|
+
});
|
|
639
|
+
return {
|
|
640
|
+
ok: false,
|
|
641
|
+
code: "MONITOR_CREDIT_UNAVAILABLE",
|
|
642
|
+
message: "Monitor run skipped because the credit service is temporarily unavailable."
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function hasAlertRule(monitor) {
|
|
647
|
+
return monitor.rules.some(
|
|
648
|
+
(rule) => isRecord(rule) && (rule.alert === true || rule.notify === true || [
|
|
649
|
+
"any_change",
|
|
650
|
+
"wallet_change",
|
|
651
|
+
"large_transfer",
|
|
652
|
+
"token_change",
|
|
653
|
+
"protocol_change",
|
|
654
|
+
"market_change",
|
|
655
|
+
"prediction_market_change"
|
|
656
|
+
].includes(String(rule.type ?? "")))
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
var RULE_SEVERITY_ORDER = {
|
|
660
|
+
low: 1,
|
|
661
|
+
medium: 2,
|
|
662
|
+
high: 3,
|
|
663
|
+
critical: 4
|
|
664
|
+
};
|
|
665
|
+
function ruleType(rule) {
|
|
666
|
+
if (!isRecord(rule)) return void 0;
|
|
667
|
+
return stringValue(rule.type) ?? stringValue(rule.metric);
|
|
668
|
+
}
|
|
669
|
+
function ruleThreshold(rule) {
|
|
670
|
+
if (!isRecord(rule)) return void 0;
|
|
671
|
+
return numberValue(rule.threshold) ?? numberValue(rule.threshold_pct) ?? numberValue(rule.min_value) ?? numberValue(rule.value);
|
|
672
|
+
}
|
|
673
|
+
function collectNumbersByKey(value, keyPatterns) {
|
|
674
|
+
const found = [];
|
|
675
|
+
const visit = (item, key = "") => {
|
|
676
|
+
if (found.length >= 100) return;
|
|
677
|
+
if (typeof item === "number" && Number.isFinite(item)) {
|
|
678
|
+
if (keyPatterns.some((pattern) => pattern.test(key))) found.push(item);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (typeof item === "string" && item.trim()) {
|
|
682
|
+
const parsed = Number(item.replace(/[$,%]/g, ""));
|
|
683
|
+
if (Number.isFinite(parsed) && keyPatterns.some((pattern) => pattern.test(key))) {
|
|
684
|
+
found.push(parsed);
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (Array.isArray(item)) {
|
|
689
|
+
item.forEach((child) => visit(child, key));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (isRecord(item)) {
|
|
693
|
+
Object.entries(item).forEach(
|
|
694
|
+
([childKey, child]) => visit(child, childKey)
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
visit(value);
|
|
699
|
+
return found;
|
|
700
|
+
}
|
|
701
|
+
function maxAbsPercentChange(previous, current) {
|
|
702
|
+
const count = Math.min(previous.length, current.length);
|
|
703
|
+
let maxChange = null;
|
|
704
|
+
for (let index = 0; index < count; index++) {
|
|
705
|
+
const before = previous[index];
|
|
706
|
+
const after = current[index];
|
|
707
|
+
if (!Number.isFinite(before) || !Number.isFinite(after) || before === 0)
|
|
708
|
+
continue;
|
|
709
|
+
const change = Math.abs((after - before) / before * 100);
|
|
710
|
+
maxChange = maxChange === null ? change : Math.max(maxChange, change);
|
|
711
|
+
}
|
|
712
|
+
return maxChange;
|
|
713
|
+
}
|
|
714
|
+
function thresholdSeverity(value, threshold) {
|
|
715
|
+
if (value >= threshold * 5) return "critical";
|
|
716
|
+
if (value >= threshold * 3) return "high";
|
|
717
|
+
if (value >= threshold * 1.5) return "medium";
|
|
718
|
+
return "low";
|
|
719
|
+
}
|
|
720
|
+
function detectRiskSeverity(snapshot) {
|
|
721
|
+
const text = sortedJson(snapshot).toLowerCase();
|
|
722
|
+
if (/\bcritical\b|is_malicious[^a-z0-9]*true|honeypot[^a-z0-9]*true|rugpull[^a-z0-9]*true/.test(
|
|
723
|
+
text
|
|
724
|
+
)) {
|
|
725
|
+
return "critical";
|
|
726
|
+
}
|
|
727
|
+
if (/\bhigh\b|blacklist[^a-z0-9]*true|phishing[^a-z0-9]*true/.test(text)) {
|
|
728
|
+
return "high";
|
|
729
|
+
}
|
|
730
|
+
if (/\bmedium\b|warning|suspicious/.test(text)) return "medium";
|
|
731
|
+
if (/\blow\b/.test(text)) return "low";
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
function severityAtLeast(actual, minimum) {
|
|
735
|
+
return RULE_SEVERITY_ORDER[actual] >= RULE_SEVERITY_ORDER[minimum];
|
|
736
|
+
}
|
|
737
|
+
function evaluateMonitorRules(args) {
|
|
738
|
+
const evaluations = [];
|
|
739
|
+
for (const rule of args.monitor.rules) {
|
|
740
|
+
const type = ruleType(rule);
|
|
741
|
+
if (!type) continue;
|
|
742
|
+
if (type === "risk_severity_at_least") {
|
|
743
|
+
const minimum = isRecord(rule) && stringValue(rule.severity) || isRecord(rule) && stringValue(rule.minimum) || "high";
|
|
744
|
+
if (!["low", "medium", "high", "critical"].includes(minimum)) continue;
|
|
745
|
+
const actual = detectRiskSeverity(args.currentSnapshot);
|
|
746
|
+
if (actual && severityAtLeast(actual, minimum)) {
|
|
747
|
+
evaluations.push({
|
|
748
|
+
kind: "rule_risk_severity_at_least",
|
|
749
|
+
severity: actual,
|
|
750
|
+
title: `${args.monitor.name} matched risk severity rule`,
|
|
751
|
+
evidence: {
|
|
752
|
+
rule,
|
|
753
|
+
actual_severity: actual,
|
|
754
|
+
minimum_severity: minimum
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const threshold = ruleThreshold(rule);
|
|
761
|
+
if (threshold === void 0 || threshold <= 0) continue;
|
|
762
|
+
const patternsByType = {
|
|
763
|
+
price_change_pct: [/price/i, /usd/i],
|
|
764
|
+
tvl_change_pct: [/tvl/i, /liquidity/i, /totalLiquidityUSD/i],
|
|
765
|
+
fee_change_pct: [/fee/i, /fees/i, /total24h/i, /total7d/i],
|
|
766
|
+
liquidity_change_pct: [
|
|
767
|
+
/liquidity/i,
|
|
768
|
+
/reserve.*usd/i,
|
|
769
|
+
/totalLiquidityUSD/i
|
|
770
|
+
],
|
|
771
|
+
transfer_value_usd: [/value.*usd/i, /amount.*usd/i, /usd/i]
|
|
772
|
+
};
|
|
773
|
+
const patterns = patternsByType[type];
|
|
774
|
+
if (!patterns) continue;
|
|
775
|
+
const currentNumbers = collectNumbersByKey(args.currentSnapshot, patterns);
|
|
776
|
+
if (type === "transfer_value_usd") {
|
|
777
|
+
const maxValue = Math.max(...currentNumbers, 0);
|
|
778
|
+
if (maxValue >= threshold) {
|
|
779
|
+
evaluations.push({
|
|
780
|
+
kind: "rule_transfer_value_usd",
|
|
781
|
+
severity: thresholdSeverity(maxValue, threshold),
|
|
782
|
+
title: `${args.monitor.name} matched transfer value rule`,
|
|
783
|
+
evidence: { rule, max_value_usd: maxValue, threshold }
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (!args.previousSnapshot) continue;
|
|
789
|
+
const previousNumbers = collectNumbersByKey(
|
|
790
|
+
args.previousSnapshot,
|
|
791
|
+
patterns
|
|
792
|
+
);
|
|
793
|
+
const maxChange = maxAbsPercentChange(previousNumbers, currentNumbers);
|
|
794
|
+
if (maxChange !== null && maxChange >= threshold) {
|
|
795
|
+
evaluations.push({
|
|
796
|
+
kind: `rule_${type}`,
|
|
797
|
+
severity: thresholdSeverity(maxChange, threshold),
|
|
798
|
+
title: `${args.monitor.name} matched ${type} rule`,
|
|
799
|
+
evidence: { rule, max_change_pct: maxChange, threshold }
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return evaluations;
|
|
804
|
+
}
|
|
805
|
+
async function executeMonitorWorkflow(monitor, executeTool, now) {
|
|
806
|
+
const definition = buildMonitorWorkflowDefinition(monitor);
|
|
807
|
+
const results = {};
|
|
808
|
+
const toolCalls = [];
|
|
809
|
+
const failedCalls = [];
|
|
810
|
+
for (const call of definition.calls) {
|
|
811
|
+
const result = await executeTool(call.toolName, call.args);
|
|
812
|
+
const record = {
|
|
813
|
+
tool: call.toolName,
|
|
814
|
+
args: call.args,
|
|
815
|
+
ok: result.ok,
|
|
816
|
+
duration_ms: result.durationMs,
|
|
817
|
+
runtime_status: result.meta.runtime_status,
|
|
818
|
+
...result.error && { error: result.error.message }
|
|
819
|
+
};
|
|
820
|
+
toolCalls.push(record);
|
|
821
|
+
if (!result.ok) failedCalls.push(record);
|
|
822
|
+
results[call.snapshotKey] = result.ok ? compactForStorage(result.data) : { error: result.error?.message ?? "Tool call failed" };
|
|
823
|
+
}
|
|
824
|
+
const snapshot = {
|
|
825
|
+
type: definition.type,
|
|
826
|
+
monitor_id: monitor.id,
|
|
827
|
+
captured_at: now.toISOString(),
|
|
828
|
+
target: definition.target,
|
|
829
|
+
results
|
|
830
|
+
};
|
|
831
|
+
return {
|
|
832
|
+
type: definition.type,
|
|
833
|
+
label: definition.label,
|
|
834
|
+
snapshot,
|
|
835
|
+
fingerprint: fingerprintSnapshot(snapshot),
|
|
836
|
+
toolCalls,
|
|
837
|
+
failedCalls
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
var SupabaseMonitorRunnerStore = class {
|
|
841
|
+
supabase() {
|
|
842
|
+
if (!isSupabaseConfigured()) {
|
|
843
|
+
throw new Error(
|
|
844
|
+
"Supabase is not configured. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY."
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
return getSupabaseAdmin();
|
|
848
|
+
}
|
|
849
|
+
async claimDueMonitors(args) {
|
|
850
|
+
const { data, error } = await this.supabase().rpc("claim_due_monitors", {
|
|
851
|
+
batch_limit: args.batchSize,
|
|
852
|
+
worker_id: args.workerId,
|
|
853
|
+
lock_timeout_seconds: args.lockTimeoutSeconds
|
|
854
|
+
});
|
|
855
|
+
if (error) {
|
|
856
|
+
secureLogger.error("claim_due_monitors failed", error, {
|
|
857
|
+
workerId: args.workerId
|
|
858
|
+
});
|
|
859
|
+
return {
|
|
860
|
+
ok: false,
|
|
861
|
+
code: "STATE_BACKEND_UNAVAILABLE",
|
|
862
|
+
message: `Failed to claim due monitors: ${error.message}`
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
return { ok: true, data: data ?? [] };
|
|
866
|
+
}
|
|
867
|
+
async createRun(args) {
|
|
868
|
+
const { data, error } = await this.supabase().from("monitor_runs").insert({
|
|
869
|
+
monitor_id: args.monitor.id,
|
|
870
|
+
user_id: args.monitor.user_id,
|
|
871
|
+
subject_id: args.monitor.subject_id,
|
|
872
|
+
status: "running",
|
|
873
|
+
source: args.source,
|
|
874
|
+
metadata: { worker_id: args.workerId }
|
|
875
|
+
}).select("*").single();
|
|
876
|
+
if (error)
|
|
877
|
+
throw new Error(`Failed to create monitor run: ${error.message}`);
|
|
878
|
+
return data;
|
|
879
|
+
}
|
|
880
|
+
async getLatestSnapshot(monitorId) {
|
|
881
|
+
const { data, error } = await this.supabase().from("monitor_snapshots").select("*").eq("monitor_id", monitorId).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
882
|
+
if (error)
|
|
883
|
+
throw new Error(`Failed to fetch latest snapshot: ${error.message}`);
|
|
884
|
+
return data ?? null;
|
|
885
|
+
}
|
|
886
|
+
async writeSnapshot(args) {
|
|
887
|
+
const { error } = await this.supabase().from("monitor_snapshots").insert({
|
|
888
|
+
monitor_id: args.monitor.id,
|
|
889
|
+
user_id: args.monitor.user_id,
|
|
890
|
+
subject_id: args.monitor.subject_id,
|
|
891
|
+
run_id: args.runId,
|
|
892
|
+
snapshot: args.snapshot,
|
|
893
|
+
fingerprint: args.fingerprint
|
|
894
|
+
});
|
|
895
|
+
if (error && error.code !== "23505") {
|
|
896
|
+
throw new Error(`Failed to write snapshot: ${error.message}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async writeObservation(args) {
|
|
900
|
+
const { error } = await this.supabase().from("observations").insert({
|
|
901
|
+
monitor_id: args.monitor.id,
|
|
902
|
+
user_id: args.monitor.user_id,
|
|
903
|
+
subject_id: args.monitor.subject_id,
|
|
904
|
+
run_id: args.runId,
|
|
905
|
+
kind: args.kind,
|
|
906
|
+
severity: args.severity,
|
|
907
|
+
title: args.title,
|
|
908
|
+
evidence: args.evidence
|
|
909
|
+
});
|
|
910
|
+
if (error) throw new Error(`Failed to write observation: ${error.message}`);
|
|
911
|
+
}
|
|
912
|
+
async writeAlert(args) {
|
|
913
|
+
const { error } = await this.supabase().from("alerts").insert({
|
|
914
|
+
monitor_id: args.monitor.id,
|
|
915
|
+
user_id: args.monitor.user_id,
|
|
916
|
+
subject_id: args.monitor.subject_id,
|
|
917
|
+
severity: args.severity,
|
|
918
|
+
title: args.title,
|
|
919
|
+
body: args.body,
|
|
920
|
+
payload: args.payload
|
|
921
|
+
});
|
|
922
|
+
if (error) throw new Error(`Failed to write alert: ${error.message}`);
|
|
923
|
+
}
|
|
924
|
+
async finishRun(args) {
|
|
925
|
+
const { error } = await this.supabase().from("monitor_runs").update({
|
|
926
|
+
status: args.status,
|
|
927
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
928
|
+
tool_calls: args.toolCalls,
|
|
929
|
+
summary: args.summary,
|
|
930
|
+
error_code: args.errorCode ?? null,
|
|
931
|
+
error_message: args.errorMessage ?? null,
|
|
932
|
+
metadata: args.metadata ?? {}
|
|
933
|
+
}).eq("id", args.runId);
|
|
934
|
+
if (error)
|
|
935
|
+
throw new Error(`Failed to finish monitor run: ${error.message}`);
|
|
936
|
+
}
|
|
937
|
+
async releaseMonitor(args) {
|
|
938
|
+
const { error } = await this.supabase().from("monitors").update({
|
|
939
|
+
last_run_at: args.lastRunAt,
|
|
940
|
+
next_run_at: args.nextRunAt,
|
|
941
|
+
locked_at: null,
|
|
942
|
+
locked_by: null,
|
|
943
|
+
...args.status && { status: args.status }
|
|
944
|
+
}).eq("id", args.monitor.id).eq("user_id", args.monitor.user_id).eq("subject_id", args.monitor.subject_id);
|
|
945
|
+
if (error) throw new Error(`Failed to release monitor: ${error.message}`);
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
async function processMonitor(monitor, options) {
|
|
949
|
+
const run = await options.store.createRun({
|
|
950
|
+
monitor,
|
|
951
|
+
workerId: options.workerId,
|
|
952
|
+
source: options.source
|
|
953
|
+
});
|
|
954
|
+
const completedAt = options.now.toISOString();
|
|
955
|
+
const nextRunAt = computeNextRunAt(monitor.cadence, options.now);
|
|
956
|
+
try {
|
|
957
|
+
const debit = await debitMonitorRunCredit({
|
|
958
|
+
monitor,
|
|
959
|
+
runId: run.id,
|
|
960
|
+
consumeCredit: options.consumeCredit
|
|
961
|
+
});
|
|
962
|
+
if (!debit.ok) {
|
|
963
|
+
await options.store.finishRun({
|
|
964
|
+
runId: run.id,
|
|
965
|
+
status: "skipped",
|
|
966
|
+
summary: debit.message,
|
|
967
|
+
toolCalls: [],
|
|
968
|
+
errorCode: debit.code,
|
|
969
|
+
errorMessage: debit.message
|
|
970
|
+
});
|
|
971
|
+
await options.store.releaseMonitor({
|
|
972
|
+
monitor,
|
|
973
|
+
lastRunAt: completedAt,
|
|
974
|
+
nextRunAt
|
|
975
|
+
});
|
|
976
|
+
return {
|
|
977
|
+
monitorId: monitor.id,
|
|
978
|
+
kind: monitor.kind,
|
|
979
|
+
status: "skipped",
|
|
980
|
+
runId: run.id,
|
|
981
|
+
summary: debit.message,
|
|
982
|
+
nextRunAt,
|
|
983
|
+
observations: 0,
|
|
984
|
+
alerts: 0
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
const previous = await options.store.getLatestSnapshot(monitor.id);
|
|
988
|
+
const workflow = await executeMonitorWorkflow(
|
|
989
|
+
monitor,
|
|
990
|
+
options.executeTool,
|
|
991
|
+
options.now
|
|
992
|
+
);
|
|
993
|
+
await options.store.writeSnapshot({
|
|
994
|
+
monitor,
|
|
995
|
+
runId: run.id,
|
|
996
|
+
snapshot: workflow.snapshot,
|
|
997
|
+
fingerprint: workflow.fingerprint
|
|
998
|
+
});
|
|
999
|
+
const ruleEvaluations = evaluateMonitorRules({
|
|
1000
|
+
monitor,
|
|
1001
|
+
previousSnapshot: previous?.snapshot ?? null,
|
|
1002
|
+
currentSnapshot: workflow.snapshot
|
|
1003
|
+
});
|
|
1004
|
+
let observations = 0;
|
|
1005
|
+
let alerts = 0;
|
|
1006
|
+
const observationPrefix = workflow.type;
|
|
1007
|
+
let summary = `${workflow.label} monitor completed with no material change.`;
|
|
1008
|
+
if (!previous) {
|
|
1009
|
+
summary = `${workflow.label} monitor baseline snapshot recorded.`;
|
|
1010
|
+
await options.store.writeObservation({
|
|
1011
|
+
monitor,
|
|
1012
|
+
runId: run.id,
|
|
1013
|
+
kind: `${observationPrefix}_baseline`,
|
|
1014
|
+
severity: "info",
|
|
1015
|
+
title: `Baseline recorded for ${monitor.name}`,
|
|
1016
|
+
evidence: { fingerprint: workflow.fingerprint }
|
|
1017
|
+
});
|
|
1018
|
+
observations++;
|
|
1019
|
+
} else if (previous.fingerprint !== workflow.fingerprint) {
|
|
1020
|
+
summary = `${workflow.label} monitor detected a snapshot change.`;
|
|
1021
|
+
const evidence = {
|
|
1022
|
+
previous_fingerprint: previous.fingerprint,
|
|
1023
|
+
current_fingerprint: workflow.fingerprint,
|
|
1024
|
+
failed_calls: workflow.failedCalls.map((call) => call.tool)
|
|
1025
|
+
};
|
|
1026
|
+
await options.store.writeObservation({
|
|
1027
|
+
monitor,
|
|
1028
|
+
runId: run.id,
|
|
1029
|
+
kind: `${observationPrefix}_change`,
|
|
1030
|
+
severity: workflow.failedCalls.length > 0 ? "medium" : "low",
|
|
1031
|
+
title: `${workflow.label} monitor changed: ${monitor.name}`,
|
|
1032
|
+
evidence
|
|
1033
|
+
});
|
|
1034
|
+
observations++;
|
|
1035
|
+
if (hasAlertRule(monitor)) {
|
|
1036
|
+
await options.store.writeAlert({
|
|
1037
|
+
monitor,
|
|
1038
|
+
severity: workflow.failedCalls.length > 0 ? "medium" : "low",
|
|
1039
|
+
title: `${workflow.label} monitor changed: ${monitor.name}`,
|
|
1040
|
+
body: `Hive detected a changed ${workflow.type} snapshot for this monitor.`,
|
|
1041
|
+
payload: evidence
|
|
1042
|
+
});
|
|
1043
|
+
alerts++;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
for (const evaluation of ruleEvaluations) {
|
|
1047
|
+
await options.store.writeObservation({
|
|
1048
|
+
monitor,
|
|
1049
|
+
runId: run.id,
|
|
1050
|
+
kind: evaluation.kind,
|
|
1051
|
+
severity: evaluation.severity,
|
|
1052
|
+
title: evaluation.title,
|
|
1053
|
+
evidence: evaluation.evidence
|
|
1054
|
+
});
|
|
1055
|
+
observations++;
|
|
1056
|
+
await options.store.writeAlert({
|
|
1057
|
+
monitor,
|
|
1058
|
+
severity: evaluation.severity,
|
|
1059
|
+
title: evaluation.title,
|
|
1060
|
+
body: "Hive matched a deterministic monitor rule threshold.",
|
|
1061
|
+
payload: evaluation.evidence
|
|
1062
|
+
});
|
|
1063
|
+
alerts++;
|
|
1064
|
+
}
|
|
1065
|
+
if (workflow.failedCalls.length > 0) {
|
|
1066
|
+
await options.store.writeObservation({
|
|
1067
|
+
monitor,
|
|
1068
|
+
runId: run.id,
|
|
1069
|
+
kind: `${observationPrefix}_monitor_degraded`,
|
|
1070
|
+
severity: "medium",
|
|
1071
|
+
title: `${workflow.label} monitor had ${workflow.failedCalls.length} failed tool call(s)`,
|
|
1072
|
+
evidence: {
|
|
1073
|
+
failed_calls: workflow.failedCalls.map((call) => ({
|
|
1074
|
+
tool: call.tool,
|
|
1075
|
+
error: call.error
|
|
1076
|
+
}))
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
observations++;
|
|
1080
|
+
}
|
|
1081
|
+
await options.store.finishRun({
|
|
1082
|
+
runId: run.id,
|
|
1083
|
+
status: "completed",
|
|
1084
|
+
summary,
|
|
1085
|
+
toolCalls: workflow.toolCalls,
|
|
1086
|
+
metadata: {
|
|
1087
|
+
fingerprint: workflow.fingerprint,
|
|
1088
|
+
failed_call_count: workflow.failedCalls.length,
|
|
1089
|
+
rule_evaluation_count: ruleEvaluations.length
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
await options.store.releaseMonitor({
|
|
1093
|
+
monitor,
|
|
1094
|
+
lastRunAt: completedAt,
|
|
1095
|
+
nextRunAt
|
|
1096
|
+
});
|
|
1097
|
+
return {
|
|
1098
|
+
monitorId: monitor.id,
|
|
1099
|
+
kind: monitor.kind,
|
|
1100
|
+
status: "completed",
|
|
1101
|
+
runId: run.id,
|
|
1102
|
+
summary,
|
|
1103
|
+
nextRunAt,
|
|
1104
|
+
observations,
|
|
1105
|
+
alerts
|
|
1106
|
+
};
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1109
|
+
await options.store.finishRun({
|
|
1110
|
+
runId: run.id,
|
|
1111
|
+
status: "failed",
|
|
1112
|
+
summary: "Monitor run failed.",
|
|
1113
|
+
toolCalls: [],
|
|
1114
|
+
errorCode: "MONITOR_RUN_FAILED",
|
|
1115
|
+
errorMessage: message
|
|
1116
|
+
});
|
|
1117
|
+
await options.store.releaseMonitor({
|
|
1118
|
+
monitor,
|
|
1119
|
+
lastRunAt: completedAt,
|
|
1120
|
+
nextRunAt
|
|
1121
|
+
});
|
|
1122
|
+
return {
|
|
1123
|
+
monitorId: monitor.id,
|
|
1124
|
+
kind: monitor.kind,
|
|
1125
|
+
status: "failed",
|
|
1126
|
+
runId: run.id,
|
|
1127
|
+
summary: message,
|
|
1128
|
+
nextRunAt,
|
|
1129
|
+
observations: 0,
|
|
1130
|
+
alerts: 0
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
async function runDueMonitors(options = {}) {
|
|
1135
|
+
const store = options.store ?? new SupabaseMonitorRunnerStore();
|
|
1136
|
+
const workerId = options.workerId ?? DEFAULT_WORKER_ID;
|
|
1137
|
+
const batchSize = Math.max(
|
|
1138
|
+
1,
|
|
1139
|
+
Math.min(options.batchSize ?? DEFAULT_BATCH_SIZE, 100)
|
|
1140
|
+
);
|
|
1141
|
+
const lockTimeoutSeconds = options.lockTimeoutSeconds ?? DEFAULT_LOCK_TIMEOUT_SECONDS;
|
|
1142
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1143
|
+
const executeTool = options.executeTool ?? executeToolByName;
|
|
1144
|
+
const consumeCredit = options.consumeCredit ?? defaultConsumeCredit;
|
|
1145
|
+
const source = options.source ?? "scheduled";
|
|
1146
|
+
const claimed = await store.claimDueMonitors({
|
|
1147
|
+
batchSize,
|
|
1148
|
+
workerId,
|
|
1149
|
+
lockTimeoutSeconds
|
|
1150
|
+
});
|
|
1151
|
+
if (!claimed.ok) {
|
|
1152
|
+
return {
|
|
1153
|
+
ok: false,
|
|
1154
|
+
claimed: 0,
|
|
1155
|
+
completed: 0,
|
|
1156
|
+
failed: 0,
|
|
1157
|
+
skipped: 0,
|
|
1158
|
+
results: [],
|
|
1159
|
+
error: claimed.message
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
const results = [];
|
|
1163
|
+
for (const monitor of claimed.data) {
|
|
1164
|
+
results.push(
|
|
1165
|
+
await processMonitor(monitor, {
|
|
1166
|
+
store,
|
|
1167
|
+
workerId,
|
|
1168
|
+
now,
|
|
1169
|
+
source,
|
|
1170
|
+
executeTool,
|
|
1171
|
+
consumeCredit
|
|
1172
|
+
})
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
ok: results.every((result) => result.status !== "failed"),
|
|
1177
|
+
claimed: claimed.data.length,
|
|
1178
|
+
completed: results.filter((result) => result.status === "completed").length,
|
|
1179
|
+
failed: results.filter((result) => result.status === "failed").length,
|
|
1180
|
+
skipped: results.filter((result) => result.status === "skipped").length,
|
|
1181
|
+
results
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/services/rwa-snapshot.ts
|
|
1186
|
+
var INSERT_CHUNK_SIZE = 500;
|
|
1187
|
+
var SNAPSHOT_MIN_GAP_MS = 55 * 60 * 1e3;
|
|
1188
|
+
function snapshotRowFor(row, capturedAt) {
|
|
1189
|
+
return {
|
|
1190
|
+
venue: row.venue,
|
|
1191
|
+
symbol: row.symbol,
|
|
1192
|
+
underlying: row.underlying,
|
|
1193
|
+
asset_class: row.asset_class,
|
|
1194
|
+
funding_rate: row.funding_rate,
|
|
1195
|
+
annualized: row.funding_annualized,
|
|
1196
|
+
mark_price: row.mark_price,
|
|
1197
|
+
oi: row.open_interest_usd,
|
|
1198
|
+
mechanism: row.funding_mechanism,
|
|
1199
|
+
flags: {
|
|
1200
|
+
market: row.market,
|
|
1201
|
+
funding_interval_hours: row.funding_interval_hours,
|
|
1202
|
+
borrow_apr_annual: row.borrow_apr_annual,
|
|
1203
|
+
is_delisted: row.is_delisted,
|
|
1204
|
+
is_price_suspect: row.is_price_suspect,
|
|
1205
|
+
price_suspect_basis: row.price_suspect_basis,
|
|
1206
|
+
price_deviation_pct: row.price_deviation_pct,
|
|
1207
|
+
volume_24h_usd: row.volume_24h_usd
|
|
1208
|
+
},
|
|
1209
|
+
captured_at: capturedAt
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
async function persistRwaSnapshot() {
|
|
1213
|
+
if (!isSupabaseConfigured()) {
|
|
1214
|
+
return {
|
|
1215
|
+
ok: true,
|
|
1216
|
+
skipped: true,
|
|
1217
|
+
reason: "supabase_not_configured",
|
|
1218
|
+
capturedRows: 0,
|
|
1219
|
+
venueResults: [],
|
|
1220
|
+
writeErrors: [],
|
|
1221
|
+
capturedAt: null
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
const { results, rows } = await collectRwaRows({
|
|
1225
|
+
hyperliquidApi: new HyperliquidAPI()
|
|
1226
|
+
});
|
|
1227
|
+
if (rows.length === 0) {
|
|
1228
|
+
return {
|
|
1229
|
+
ok: false,
|
|
1230
|
+
skipped: false,
|
|
1231
|
+
reason: "no_rows_from_any_venue",
|
|
1232
|
+
capturedRows: 0,
|
|
1233
|
+
venueResults: results,
|
|
1234
|
+
writeErrors: [],
|
|
1235
|
+
capturedAt: null
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
const supabase = getSupabaseAdmin();
|
|
1239
|
+
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1240
|
+
const payload = rows.map((row) => snapshotRowFor(row, capturedAt));
|
|
1241
|
+
const writeErrors = [];
|
|
1242
|
+
let insertedCount = 0;
|
|
1243
|
+
for (let i = 0; i < payload.length; i += INSERT_CHUNK_SIZE) {
|
|
1244
|
+
const chunk = payload.slice(i, i + INSERT_CHUNK_SIZE);
|
|
1245
|
+
const { error } = await supabase.from("rwa_funding_snapshots").insert(chunk);
|
|
1246
|
+
if (error) {
|
|
1247
|
+
writeErrors.push(`insert chunk ${i}: ${error.message}`);
|
|
1248
|
+
} else {
|
|
1249
|
+
insertedCount += chunk.length;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const ok = writeErrors.length === 0;
|
|
1253
|
+
if (!ok) {
|
|
1254
|
+
secureLogger.warn("RWA snapshot write errors", {
|
|
1255
|
+
errorCount: writeErrors.length
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
ok,
|
|
1260
|
+
skipped: false,
|
|
1261
|
+
capturedRows: insertedCount,
|
|
1262
|
+
venueResults: results,
|
|
1263
|
+
writeErrors,
|
|
1264
|
+
capturedAt
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
async function runScheduledRwaSnapshot() {
|
|
1268
|
+
try {
|
|
1269
|
+
if ((getEnvVar("HIVE_RWA_SNAPSHOT_ENABLED") ?? "true") === "false") {
|
|
1270
|
+
return {
|
|
1271
|
+
ok: true,
|
|
1272
|
+
skipped: true,
|
|
1273
|
+
reason: "disabled_by_env",
|
|
1274
|
+
capturedRows: 0,
|
|
1275
|
+
venueResults: [],
|
|
1276
|
+
writeErrors: [],
|
|
1277
|
+
capturedAt: null
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
if (!isSupabaseConfigured()) {
|
|
1281
|
+
return {
|
|
1282
|
+
ok: true,
|
|
1283
|
+
skipped: true,
|
|
1284
|
+
reason: "supabase_not_configured",
|
|
1285
|
+
capturedRows: 0,
|
|
1286
|
+
venueResults: [],
|
|
1287
|
+
writeErrors: [],
|
|
1288
|
+
capturedAt: null
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
const supabase = getSupabaseAdmin();
|
|
1292
|
+
const { data, error } = await supabase.from("rwa_funding_snapshots").select("captured_at").order("captured_at", { ascending: false }).limit(1);
|
|
1293
|
+
if (error) {
|
|
1294
|
+
return {
|
|
1295
|
+
ok: false,
|
|
1296
|
+
skipped: false,
|
|
1297
|
+
reason: `cadence check failed: ${error.message}`,
|
|
1298
|
+
capturedRows: 0,
|
|
1299
|
+
venueResults: [],
|
|
1300
|
+
writeErrors: [error.message],
|
|
1301
|
+
capturedAt: null
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
const latest = data?.[0]?.captured_at ? Date.parse(String(data[0].captured_at)) : null;
|
|
1305
|
+
if (latest != null && Number.isFinite(latest) && Date.now() - latest < SNAPSHOT_MIN_GAP_MS) {
|
|
1306
|
+
return {
|
|
1307
|
+
ok: true,
|
|
1308
|
+
skipped: true,
|
|
1309
|
+
reason: "within_cadence_gap",
|
|
1310
|
+
capturedRows: 0,
|
|
1311
|
+
venueResults: [],
|
|
1312
|
+
writeErrors: [],
|
|
1313
|
+
capturedAt: null
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
return await persistRwaSnapshot();
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1319
|
+
return {
|
|
1320
|
+
ok: false,
|
|
1321
|
+
skipped: false,
|
|
1322
|
+
reason: message,
|
|
1323
|
+
capturedRows: 0,
|
|
1324
|
+
venueResults: [],
|
|
1325
|
+
writeErrors: [message],
|
|
1326
|
+
capturedAt: null
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/monitor-worker.ts
|
|
1332
|
+
function intEnv(name, fallback) {
|
|
1333
|
+
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
|
1334
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
1335
|
+
}
|
|
1336
|
+
async function runMonitorWorkerFromEnv() {
|
|
1337
|
+
try {
|
|
1338
|
+
const result = await runDueMonitors({
|
|
1339
|
+
batchSize: intEnv("HIVE_MONITOR_WORKER_BATCH_SIZE", 10),
|
|
1340
|
+
workerId: process.env.HIVE_MONITOR_WORKER_ID,
|
|
1341
|
+
lockTimeoutSeconds: intEnv("HIVE_MONITOR_LOCK_TIMEOUT_SECONDS", 300)
|
|
1342
|
+
}).catch((error) => ({
|
|
1343
|
+
ok: false,
|
|
1344
|
+
claimed: 0,
|
|
1345
|
+
completed: 0,
|
|
1346
|
+
failed: 0,
|
|
1347
|
+
skipped: 0,
|
|
1348
|
+
results: [],
|
|
1349
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1350
|
+
}));
|
|
1351
|
+
const scheduledHealthCanaries = await runScheduledHealthCanaries().catch(
|
|
1352
|
+
(error) => ({
|
|
1353
|
+
ok: false,
|
|
1354
|
+
skipped: false,
|
|
1355
|
+
persistent: false,
|
|
1356
|
+
duration: 0,
|
|
1357
|
+
canaries: {},
|
|
1358
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
1359
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1360
|
+
})
|
|
1361
|
+
);
|
|
1362
|
+
const rwaSnapshot = await runScheduledRwaSnapshot();
|
|
1363
|
+
console.log(
|
|
1364
|
+
JSON.stringify(
|
|
1365
|
+
{
|
|
1366
|
+
service: "hive-monitor-worker",
|
|
1367
|
+
worker_id: process.env.HIVE_MONITOR_WORKER_ID ?? null,
|
|
1368
|
+
...result,
|
|
1369
|
+
scheduled_health_canaries: scheduledHealthCanaries,
|
|
1370
|
+
rwa_snapshot: rwaSnapshot
|
|
1371
|
+
},
|
|
1372
|
+
null,
|
|
1373
|
+
2
|
|
1374
|
+
)
|
|
1375
|
+
);
|
|
1376
|
+
if (!result.ok || !scheduledHealthCanaries.ok) process.exitCode = 1;
|
|
1377
|
+
} finally {
|
|
1378
|
+
await closeRedisClient();
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
var invokedPath = process.argv[1] ? resolve(process.argv[1]) : "";
|
|
1382
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
1383
|
+
runMonitorWorkerFromEnv().catch((error) => {
|
|
1384
|
+
console.error(
|
|
1385
|
+
JSON.stringify({
|
|
1386
|
+
service: "hive-monitor-worker",
|
|
1387
|
+
ok: false,
|
|
1388
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1389
|
+
})
|
|
1390
|
+
);
|
|
1391
|
+
process.exitCode = 1;
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
export {
|
|
1395
|
+
runMonitorWorkerFromEnv
|
|
1396
|
+
};
|