@vizzor/cli 0.10.0 → 0.11.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/dist/index.js CHANGED
@@ -62,18 +62,31 @@ var init_schema = __esm({
62
62
  api: z.object({
63
63
  port: z.number().default(3e3),
64
64
  host: z.string().default("0.0.0.0"),
65
- enableAuth: z.boolean().default(false),
66
- corsOrigin: z.string().default("*")
65
+ enableAuth: z.boolean().default(true),
66
+ corsOrigin: z.string().default("http://localhost:3000")
67
67
  }).default(() => ({
68
68
  port: 3e3,
69
69
  host: "0.0.0.0",
70
- enableAuth: false,
71
- corsOrigin: "*"
70
+ enableAuth: true,
71
+ corsOrigin: "http://localhost:3000"
72
72
  })),
73
73
  n8n: z.object({
74
74
  enabled: z.boolean().default(false),
75
75
  webhookUrl: z.string().optional()
76
76
  }).default(() => ({ enabled: false })),
77
+ realtime: z.object({
78
+ enabled: z.boolean().default(false),
79
+ provider: z.enum(["binance"]).default("binance"),
80
+ symbols: z.array(z.string()).default([])
81
+ }).optional(),
82
+ trading: z.object({
83
+ enabled: z.boolean().default(false),
84
+ maxSlippage: z.number().default(0.5),
85
+ gasMultiplier: z.number().default(1.2),
86
+ confirmBeforeExecute: z.boolean().default(true),
87
+ dryRun: z.boolean().default(true),
88
+ walletName: z.string().optional()
89
+ }).optional(),
77
90
  discordToken: z.string().optional(),
78
91
  discordGuildId: z.string().optional(),
79
92
  telegramToken: z.string().optional()
@@ -82,7 +95,15 @@ var init_schema = __esm({
82
95
  });
83
96
 
84
97
  // src/config/loader.ts
85
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
98
+ var loader_exports = {};
99
+ __export(loader_exports, {
100
+ getConfig: () => getConfig,
101
+ getConfigDir: () => getConfigDir,
102
+ getSettableKeys: () => getSettableKeys,
103
+ loadConfig: () => loadConfig,
104
+ saveConfigValue: () => saveConfigValue
105
+ });
106
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
86
107
  import { join } from "path";
87
108
  import { homedir } from "os";
88
109
  import yaml from "yaml";
@@ -209,7 +230,8 @@ function saveConfigValue(key, value) {
209
230
  raw[key] = value;
210
231
  }
211
232
  vizzorConfigSchema.parse(raw);
212
- writeFileSync(configPath, yaml.stringify(raw), "utf-8");
233
+ writeFileSync(configPath, yaml.stringify(raw), { encoding: "utf-8", mode: 384 });
234
+ chmodSync(configPath, 384);
213
235
  cachedConfig = null;
214
236
  loadConfig();
215
237
  }
@@ -250,6 +272,20 @@ var init_loader = __esm({
250
272
  }
251
273
  });
252
274
 
275
+ // src/utils/validate.ts
276
+ function assertValidAddress(address) {
277
+ if (!EVM_ADDRESS_RE.test(address)) {
278
+ throw new Error(`Invalid address format: expected 0x followed by 40 hex characters`);
279
+ }
280
+ }
281
+ var EVM_ADDRESS_RE;
282
+ var init_validate = __esm({
283
+ "src/utils/validate.ts"() {
284
+ "use strict";
285
+ EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
286
+ }
287
+ });
288
+
253
289
  // src/chains/evm/abi/erc20.ts
254
290
  var erc20Abi;
255
291
  var init_erc20 = __esm({
@@ -595,11 +631,12 @@ import {
595
631
  createPublicClient,
596
632
  http
597
633
  } from "viem";
598
- import { mainnet, polygon, arbitrum, optimism, base } from "viem/chains";
634
+ import { mainnet, polygon, arbitrum, optimism, base, bsc, avalanche } from "viem/chains";
599
635
  var CHAIN_MAP, EvmAdapter;
600
636
  var init_adapter = __esm({
601
637
  "src/chains/evm/adapter.ts"() {
602
638
  "use strict";
639
+ init_validate();
603
640
  init_erc20();
604
641
  init_etherscan();
605
642
  init_constants();
@@ -608,7 +645,9 @@ var init_adapter = __esm({
608
645
  polygon,
609
646
  arbitrum,
610
647
  optimism,
611
- base
648
+ base,
649
+ bsc,
650
+ avalanche
612
651
  };
613
652
  EvmAdapter = class {
614
653
  chainId;
@@ -651,18 +690,23 @@ var init_adapter = __esm({
651
690
  isConnected() {
652
691
  return this.client !== null;
653
692
  }
693
+ /** Validate and cast to viem Address. */
694
+ toAddress(address) {
695
+ assertValidAddress(address);
696
+ return this.toAddress(address);
697
+ }
654
698
  // ── Balances ────────────────────────────────────────────────────────────
655
699
  async getBalance(address) {
656
700
  const client2 = this.requireClient();
657
- return client2.getBalance({ address });
701
+ return client2.getBalance({ address: this.toAddress(address) });
658
702
  }
659
703
  async getTokenBalance(address, tokenAddress) {
660
704
  const client2 = this.requireClient();
661
705
  const balance = await client2.readContract({
662
- address: tokenAddress,
706
+ address: this.toAddress(tokenAddress),
663
707
  abi: erc20Abi,
664
708
  functionName: "balanceOf",
665
- args: [address]
709
+ args: [this.toAddress(address)]
666
710
  });
667
711
  return balance;
668
712
  }
@@ -687,13 +731,13 @@ var init_adapter = __esm({
687
731
  // ── Contracts ───────────────────────────────────────────────────────────
688
732
  async getContractCode(address) {
689
733
  const client2 = this.requireClient();
690
- const code = await client2.getCode({ address });
734
+ const code = await client2.getCode({ address: this.toAddress(address) });
691
735
  return code ?? "0x";
692
736
  }
693
737
  async readContract(address, abi, functionName, args2) {
694
738
  const client2 = this.requireClient();
695
739
  return client2.readContract({
696
- address,
740
+ address: this.toAddress(address),
697
741
  abi,
698
742
  functionName,
699
743
  args: args2
@@ -702,25 +746,25 @@ var init_adapter = __esm({
702
746
  async getContractEvents(address, abi, eventName, options) {
703
747
  const client2 = this.requireClient();
704
748
  const logs = await client2.getContractEvents({
705
- address,
749
+ address: this.toAddress(address),
706
750
  abi,
707
751
  eventName,
708
752
  fromBlock: options?.fromBlock,
709
753
  toBlock: options?.toBlock,
710
754
  args: options?.args
711
755
  });
712
- return logs.map((log4) => ({
713
- eventName: log4.eventName ?? eventName,
714
- blockNumber: log4.blockNumber ?? 0n,
715
- transactionHash: log4.transactionHash ?? "0x",
716
- args: log4.args ?? {},
717
- logIndex: log4.logIndex ?? 0
756
+ return logs.map((log15) => ({
757
+ eventName: log15.eventName ?? eventName,
758
+ blockNumber: log15.blockNumber ?? 0n,
759
+ transactionHash: log15.transactionHash ?? "0x",
760
+ args: log15.args ?? {},
761
+ logIndex: log15.logIndex ?? 0
718
762
  }));
719
763
  }
720
764
  // ── Tokens ──────────────────────────────────────────────────────────────
721
765
  async getTokenInfo(address) {
722
766
  const client2 = this.requireClient();
723
- const tokenAddr = address;
767
+ const tokenAddr = this.toAddress(address);
724
768
  const [name, symbol, decimals, totalSupply] = await Promise.all([
725
769
  client2.readContract({
726
770
  address: tokenAddr,
@@ -779,6 +823,46 @@ var init_adapter = __esm({
779
823
  }
780
824
  });
781
825
 
826
+ // src/utils/logger.ts
827
+ import pino from "pino";
828
+ function createLogger(name) {
829
+ const isDev = process.env["NODE_ENV"] !== "production";
830
+ if (isDev) {
831
+ return pino({
832
+ name,
833
+ level,
834
+ transport: {
835
+ target: "pino-pretty",
836
+ options: {
837
+ colorize: true
838
+ }
839
+ }
840
+ });
841
+ }
842
+ return pino({ name, level });
843
+ }
844
+ var level;
845
+ var init_logger = __esm({
846
+ "src/utils/logger.ts"() {
847
+ "use strict";
848
+ level = process.env["VIZZOR_LOG_LEVEL"] ?? "info";
849
+ }
850
+ });
851
+
852
+ // src/chains/evm/writable-adapter.ts
853
+ import { createWalletClient, http as http2 } from "viem";
854
+ import { privateKeyToAccount } from "viem/accounts";
855
+ import { mainnet as mainnet2, polygon as polygon2, arbitrum as arbitrum2, optimism as optimism2, base as base2, bsc as bsc2, avalanche as avalanche2 } from "viem/chains";
856
+ var log;
857
+ var init_writable_adapter = __esm({
858
+ "src/chains/evm/writable-adapter.ts"() {
859
+ "use strict";
860
+ init_adapter();
861
+ init_logger();
862
+ log = createLogger("writable-evm");
863
+ }
864
+ });
865
+
782
866
  // src/chains/zk/adapter.ts
783
867
  function getZkChainIds() {
784
868
  return Object.keys(ZK_CHAIN_CONFIG);
@@ -836,6 +920,457 @@ var init_adapter2 = __esm({
836
920
  }
837
921
  });
838
922
 
923
+ // src/chains/solana/adapter.ts
924
+ var log2, SolanaAdapter;
925
+ var init_adapter3 = __esm({
926
+ "src/chains/solana/adapter.ts"() {
927
+ "use strict";
928
+ init_logger();
929
+ log2 = createLogger("solana-adapter");
930
+ SolanaAdapter = class {
931
+ chainId = "solana";
932
+ name = "Solana";
933
+ nativeCurrency = { symbol: "SOL", decimals: 9 };
934
+ rpcUrl = "https://api.mainnet-beta.solana.com";
935
+ connected = false;
936
+ async connect(rpcUrl) {
937
+ if (rpcUrl) this.rpcUrl = rpcUrl;
938
+ this.connected = true;
939
+ log2.info(`Connected to Solana: ${this.rpcUrl}`);
940
+ }
941
+ async disconnect() {
942
+ this.connected = false;
943
+ }
944
+ isConnected() {
945
+ return this.connected;
946
+ }
947
+ async rpc(method, params = []) {
948
+ const res = await fetch(this.rpcUrl, {
949
+ method: "POST",
950
+ headers: { "Content-Type": "application/json" },
951
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
952
+ signal: AbortSignal.timeout(15e3)
953
+ });
954
+ const json = await res.json();
955
+ if (json.error) throw new Error(`Solana RPC: ${json.error.message}`);
956
+ return json.result;
957
+ }
958
+ async getBalance(address) {
959
+ const result = await this.rpc("getBalance", [address]);
960
+ return BigInt(result.value);
961
+ }
962
+ async getTokenBalance(address, tokenAddress) {
963
+ const result = await this.rpc("getTokenAccountsByOwner", [
964
+ address,
965
+ { mint: tokenAddress },
966
+ { encoding: "jsonParsed" }
967
+ ]);
968
+ if (result.value.length === 0) return 0n;
969
+ return BigInt(result.value[0].account.data.parsed.info.tokenAmount.amount);
970
+ }
971
+ async getTransactionHistory(address, options) {
972
+ const limit = options?.limit ?? 20;
973
+ const sigs = await this.rpc("getSignaturesForAddress", [address, { limit }]);
974
+ return sigs.map((s) => ({
975
+ hash: s.signature,
976
+ blockNumber: BigInt(s.slot),
977
+ from: address,
978
+ to: null,
979
+ value: 0n,
980
+ gasUsed: 0n,
981
+ gasPrice: 0n,
982
+ timestamp: s.blockTime ?? 0,
983
+ status: s.err ? "reverted" : "success",
984
+ input: ""
985
+ }));
986
+ }
987
+ async getTokenTransfers(_address, _options) {
988
+ return [];
989
+ }
990
+ async getContractCode(address) {
991
+ const result = await this.rpc("getAccountInfo", [address, { encoding: "base64" }]);
992
+ return result?.value?.data?.[0] ?? "";
993
+ }
994
+ async readContract(_address, _abi, _functionName, _args) {
995
+ throw new Error("readContract not supported on Solana \u2014 use program-specific methods");
996
+ }
997
+ async getContractEvents(_address, _abi, _eventName, _options) {
998
+ return [];
999
+ }
1000
+ async getTokenInfo(address) {
1001
+ const result = await this.rpc("getAccountInfo", [address, { encoding: "jsonParsed" }]);
1002
+ const info = result?.value?.data?.parsed?.info;
1003
+ return {
1004
+ address,
1005
+ name: "SPL Token",
1006
+ symbol: "SPL",
1007
+ decimals: info?.decimals ?? 9,
1008
+ totalSupply: BigInt(info?.supply ?? "0")
1009
+ };
1010
+ }
1011
+ async getTopHolders(_tokenAddress, _limit) {
1012
+ return [];
1013
+ }
1014
+ async getBlockNumber() {
1015
+ const slot = await this.rpc("getSlot");
1016
+ return BigInt(slot);
1017
+ }
1018
+ async getBlock(blockNumber) {
1019
+ const result = await this.rpc("getBlock", [
1020
+ Number(blockNumber),
1021
+ { transactionDetails: "none" }
1022
+ ]);
1023
+ return {
1024
+ number: blockNumber,
1025
+ hash: result?.blockhash ?? "",
1026
+ parentHash: "",
1027
+ timestamp: result?.blockTime ?? 0,
1028
+ gasUsed: 0n,
1029
+ gasLimit: 0n,
1030
+ baseFeePerGas: null,
1031
+ transactionCount: result?.transactions?.length ?? 0
1032
+ };
1033
+ }
1034
+ };
1035
+ }
1036
+ });
1037
+
1038
+ // src/chains/sui/adapter.ts
1039
+ var log3, SuiAdapter;
1040
+ var init_adapter4 = __esm({
1041
+ "src/chains/sui/adapter.ts"() {
1042
+ "use strict";
1043
+ init_logger();
1044
+ log3 = createLogger("sui-adapter");
1045
+ SuiAdapter = class {
1046
+ chainId = "sui";
1047
+ name = "Sui";
1048
+ nativeCurrency = { symbol: "SUI", decimals: 9 };
1049
+ rpcUrl = "https://fullnode.mainnet.sui.io:443";
1050
+ connected = false;
1051
+ async connect(rpcUrl) {
1052
+ if (rpcUrl) this.rpcUrl = rpcUrl;
1053
+ this.connected = true;
1054
+ log3.info(`Connected to Sui: ${this.rpcUrl}`);
1055
+ }
1056
+ async disconnect() {
1057
+ this.connected = false;
1058
+ }
1059
+ isConnected() {
1060
+ return this.connected;
1061
+ }
1062
+ async rpc(method, params = []) {
1063
+ const res = await fetch(this.rpcUrl, {
1064
+ method: "POST",
1065
+ headers: { "Content-Type": "application/json" },
1066
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
1067
+ signal: AbortSignal.timeout(15e3)
1068
+ });
1069
+ const json = await res.json();
1070
+ if (json.error) throw new Error(`Sui RPC: ${json.error.message}`);
1071
+ return json.result;
1072
+ }
1073
+ async getBalance(address) {
1074
+ const result = await this.rpc("suix_getBalance", [address, "0x2::sui::SUI"]);
1075
+ return BigInt(result.totalBalance);
1076
+ }
1077
+ async getTokenBalance(address, tokenAddress) {
1078
+ const result = await this.rpc("suix_getBalance", [address, tokenAddress]);
1079
+ return BigInt(result.totalBalance);
1080
+ }
1081
+ async getTransactionHistory(address, options) {
1082
+ const limit = options?.limit ?? 20;
1083
+ const result = await this.rpc("suix_queryTransactionBlocks", [
1084
+ { filter: { FromAddress: address } },
1085
+ null,
1086
+ limit,
1087
+ true
1088
+ ]);
1089
+ return (result.data ?? []).map((tx) => ({
1090
+ hash: tx.digest,
1091
+ blockNumber: BigInt(tx.checkpoint ?? "0"),
1092
+ from: address,
1093
+ to: null,
1094
+ value: 0n,
1095
+ gasUsed: 0n,
1096
+ gasPrice: 0n,
1097
+ timestamp: Math.floor(Number(tx.timestampMs) / 1e3),
1098
+ status: "success",
1099
+ input: ""
1100
+ }));
1101
+ }
1102
+ async getTokenTransfers(_address, _options) {
1103
+ return [];
1104
+ }
1105
+ async getContractCode(address) {
1106
+ const result = await this.rpc("sui_getObject", [address, { showContent: true }]);
1107
+ return result.data?.content?.type ?? "";
1108
+ }
1109
+ async readContract(_address, _abi, _functionName, _args) {
1110
+ throw new Error("readContract not supported on Sui \u2014 use Move call methods");
1111
+ }
1112
+ async getContractEvents(_address, _abi, _eventName, _options) {
1113
+ return [];
1114
+ }
1115
+ async getTokenInfo(address) {
1116
+ const result = await this.rpc("suix_getCoinMetadata", [address]);
1117
+ return {
1118
+ address,
1119
+ name: result?.name ?? "Unknown",
1120
+ symbol: result?.symbol ?? "???",
1121
+ decimals: result?.decimals ?? 9,
1122
+ totalSupply: 0n
1123
+ };
1124
+ }
1125
+ async getTopHolders(_tokenAddress, _limit) {
1126
+ return [];
1127
+ }
1128
+ async getBlockNumber() {
1129
+ const result = await this.rpc("sui_getLatestCheckpointSequenceNumber");
1130
+ return BigInt(result);
1131
+ }
1132
+ async getBlock(blockNumber) {
1133
+ const result = await this.rpc("sui_getCheckpoint", [String(blockNumber)]);
1134
+ return {
1135
+ number: blockNumber,
1136
+ hash: result?.digest ?? "",
1137
+ parentHash: result?.previousDigest ?? "",
1138
+ timestamp: result ? Math.floor(Number(result.timestampMs) / 1e3) : 0,
1139
+ gasUsed: 0n,
1140
+ gasLimit: 0n,
1141
+ baseFeePerGas: null,
1142
+ transactionCount: result?.transactions?.length ?? 0
1143
+ };
1144
+ }
1145
+ };
1146
+ }
1147
+ });
1148
+
1149
+ // src/chains/aptos/adapter.ts
1150
+ var log4, AptosAdapter;
1151
+ var init_adapter5 = __esm({
1152
+ "src/chains/aptos/adapter.ts"() {
1153
+ "use strict";
1154
+ init_logger();
1155
+ log4 = createLogger("aptos-adapter");
1156
+ AptosAdapter = class {
1157
+ chainId = "aptos";
1158
+ name = "Aptos";
1159
+ nativeCurrency = { symbol: "APT", decimals: 8 };
1160
+ apiUrl = "https://fullnode.mainnet.aptoslabs.com/v1";
1161
+ connected = false;
1162
+ async connect(rpcUrl) {
1163
+ if (rpcUrl) this.apiUrl = rpcUrl;
1164
+ this.connected = true;
1165
+ log4.info(`Connected to Aptos: ${this.apiUrl}`);
1166
+ }
1167
+ async disconnect() {
1168
+ this.connected = false;
1169
+ }
1170
+ isConnected() {
1171
+ return this.connected;
1172
+ }
1173
+ async api(path) {
1174
+ const res = await fetch(`${this.apiUrl}${path}`, {
1175
+ signal: AbortSignal.timeout(15e3)
1176
+ });
1177
+ if (!res.ok) throw new Error(`Aptos API: ${res.status} ${res.statusText}`);
1178
+ return res.json();
1179
+ }
1180
+ async getBalance(address) {
1181
+ try {
1182
+ const result = await this.api(
1183
+ `/accounts/${address}/resource/0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>`
1184
+ );
1185
+ return BigInt(result.data.coin.value);
1186
+ } catch {
1187
+ return 0n;
1188
+ }
1189
+ }
1190
+ async getTokenBalance(address, tokenAddress) {
1191
+ try {
1192
+ const result = await this.api(
1193
+ `/accounts/${address}/resource/0x1::coin::CoinStore<${tokenAddress}>`
1194
+ );
1195
+ return BigInt(result.data.coin.value);
1196
+ } catch {
1197
+ return 0n;
1198
+ }
1199
+ }
1200
+ async getTransactionHistory(address, options) {
1201
+ const limit = options?.limit ?? 20;
1202
+ const txns = await this.api(`/accounts/${address}/transactions?limit=${limit}`);
1203
+ return txns.map((tx) => ({
1204
+ hash: tx.hash,
1205
+ blockNumber: BigInt(tx.version),
1206
+ from: tx.sender,
1207
+ to: null,
1208
+ value: 0n,
1209
+ gasUsed: BigInt(tx.gas_used),
1210
+ gasPrice: BigInt(tx.gas_unit_price),
1211
+ timestamp: Math.floor(Number(tx.timestamp) / 1e6),
1212
+ status: tx.success ? "success" : "reverted",
1213
+ input: ""
1214
+ }));
1215
+ }
1216
+ async getTokenTransfers(_address, _options) {
1217
+ return [];
1218
+ }
1219
+ async getContractCode(address) {
1220
+ try {
1221
+ const result = await this.api(`/accounts/${address}/modules`);
1222
+ return result.map((m) => m.bytecode).join("");
1223
+ } catch {
1224
+ return "";
1225
+ }
1226
+ }
1227
+ async readContract(_address, _abi, _functionName, _args) {
1228
+ throw new Error("readContract not supported on Aptos \u2014 use Move view functions");
1229
+ }
1230
+ async getContractEvents(_address, _abi, _eventName, _options) {
1231
+ return [];
1232
+ }
1233
+ async getTokenInfo(address) {
1234
+ return {
1235
+ address,
1236
+ name: "Move Coin",
1237
+ symbol: "MOVE",
1238
+ decimals: 8,
1239
+ totalSupply: 0n
1240
+ };
1241
+ }
1242
+ async getTopHolders(_tokenAddress, _limit) {
1243
+ return [];
1244
+ }
1245
+ async getBlockNumber() {
1246
+ const result = await this.api("/");
1247
+ return BigInt(result.ledger_version);
1248
+ }
1249
+ async getBlock(blockNumber) {
1250
+ const result = await this.api(`/blocks/by_version/${blockNumber}`);
1251
+ return {
1252
+ number: BigInt(result.block_height),
1253
+ hash: result.block_hash,
1254
+ parentHash: "",
1255
+ timestamp: Math.floor(Number(result.block_timestamp) / 1e6),
1256
+ gasUsed: 0n,
1257
+ gasLimit: 0n,
1258
+ baseFeePerGas: null,
1259
+ transactionCount: result.transactions?.length ?? 0
1260
+ };
1261
+ }
1262
+ };
1263
+ }
1264
+ });
1265
+
1266
+ // src/chains/ton/adapter.ts
1267
+ var log5, TonAdapter;
1268
+ var init_adapter6 = __esm({
1269
+ "src/chains/ton/adapter.ts"() {
1270
+ "use strict";
1271
+ init_logger();
1272
+ log5 = createLogger("ton-adapter");
1273
+ TonAdapter = class {
1274
+ chainId = "ton";
1275
+ name = "TON";
1276
+ nativeCurrency = { symbol: "TON", decimals: 9 };
1277
+ apiUrl = "https://toncenter.com/api/v2";
1278
+ connected = false;
1279
+ async connect(rpcUrl) {
1280
+ if (rpcUrl) this.apiUrl = rpcUrl;
1281
+ this.connected = true;
1282
+ log5.info(`Connected to TON: ${this.apiUrl}`);
1283
+ }
1284
+ async disconnect() {
1285
+ this.connected = false;
1286
+ }
1287
+ isConnected() {
1288
+ return this.connected;
1289
+ }
1290
+ async api(method, params = {}) {
1291
+ const qs = new URLSearchParams(params).toString();
1292
+ const url = `${this.apiUrl}/${method}${qs ? `?${qs}` : ""}`;
1293
+ const res = await fetch(url, { signal: AbortSignal.timeout(15e3) });
1294
+ if (!res.ok) throw new Error(`TON API: ${res.status}`);
1295
+ const json = await res.json();
1296
+ if (!json.ok) throw new Error(`TON API: ${json.error ?? "unknown error"}`);
1297
+ return json.result;
1298
+ }
1299
+ async getBalance(address) {
1300
+ const result = await this.api("getAddressBalance", { address });
1301
+ return BigInt(result);
1302
+ }
1303
+ async getTokenBalance(_address, _tokenAddress) {
1304
+ return 0n;
1305
+ }
1306
+ async getTransactionHistory(address, options) {
1307
+ const limit = options?.limit ?? 20;
1308
+ const txns = await this.api("getTransactions", {
1309
+ address,
1310
+ limit: String(limit)
1311
+ });
1312
+ return txns.map((tx) => ({
1313
+ hash: tx.transaction_id.hash,
1314
+ blockNumber: 0n,
1315
+ from: tx.in_msg?.source ?? address,
1316
+ to: tx.in_msg?.destination ?? null,
1317
+ value: BigInt(tx.in_msg?.value ?? "0"),
1318
+ gasUsed: BigInt(tx.fee),
1319
+ gasPrice: 0n,
1320
+ timestamp: tx.utime,
1321
+ status: "success",
1322
+ input: ""
1323
+ }));
1324
+ }
1325
+ async getTokenTransfers(_address, _options) {
1326
+ return [];
1327
+ }
1328
+ async getContractCode(address) {
1329
+ try {
1330
+ const result = await this.api("getAddressInformation", { address });
1331
+ return result.code ?? "";
1332
+ } catch {
1333
+ return "";
1334
+ }
1335
+ }
1336
+ async readContract(_address, _abi, _functionName, _args) {
1337
+ throw new Error("readContract not supported on TON \u2014 use get methods");
1338
+ }
1339
+ async getContractEvents(_address, _abi, _eventName, _options) {
1340
+ return [];
1341
+ }
1342
+ async getTokenInfo(_address) {
1343
+ return {
1344
+ address: _address,
1345
+ name: "Jetton",
1346
+ symbol: "JET",
1347
+ decimals: 9,
1348
+ totalSupply: 0n
1349
+ };
1350
+ }
1351
+ async getTopHolders(_tokenAddress, _limit) {
1352
+ return [];
1353
+ }
1354
+ async getBlockNumber() {
1355
+ const result = await this.api("getMasterchainInfo");
1356
+ return BigInt(result.last.seqno);
1357
+ }
1358
+ async getBlock(blockNumber) {
1359
+ return {
1360
+ number: blockNumber,
1361
+ hash: "",
1362
+ parentHash: "",
1363
+ timestamp: 0,
1364
+ gasUsed: 0n,
1365
+ gasLimit: 0n,
1366
+ baseFeePerGas: null,
1367
+ transactionCount: 0
1368
+ };
1369
+ }
1370
+ };
1371
+ }
1372
+ });
1373
+
839
1374
  // src/chains/registry.ts
840
1375
  function getAdapter(chainId) {
841
1376
  const factory = registry.get(chainId);
@@ -854,7 +1389,12 @@ var init_registry = __esm({
854
1389
  "src/chains/registry.ts"() {
855
1390
  "use strict";
856
1391
  init_adapter();
1392
+ init_writable_adapter();
857
1393
  init_adapter2();
1394
+ init_adapter3();
1395
+ init_adapter4();
1396
+ init_adapter5();
1397
+ init_adapter6();
858
1398
  registry = /* @__PURE__ */ new Map();
859
1399
  evmFactory = (chainId) => new EvmAdapter(chainId);
860
1400
  BUILTIN_EVM_CHAINS = ["ethereum", "polygon", "arbitrum", "optimism", "base"];
@@ -865,32 +1405,12 @@ var init_registry = __esm({
865
1405
  for (const chainId of getZkChainIds()) {
866
1406
  registry.set(chainId, zkFactory);
867
1407
  }
868
- }
869
- });
870
-
871
- // src/utils/logger.ts
872
- import pino from "pino";
873
- function createLogger(name) {
874
- const isDev = process.env["NODE_ENV"] !== "production";
875
- if (isDev) {
876
- return pino({
877
- name,
878
- level,
879
- transport: {
880
- target: "pino-pretty",
881
- options: {
882
- colorize: true
883
- }
884
- }
885
- });
886
- }
887
- return pino({ name, level });
888
- }
889
- var level;
890
- var init_logger = __esm({
891
- "src/utils/logger.ts"() {
892
- "use strict";
893
- level = process.env["VIZZOR_LOG_LEVEL"] ?? "info";
1408
+ registry.set("bsc", evmFactory);
1409
+ registry.set("avalanche", evmFactory);
1410
+ registry.set("solana", () => new SolanaAdapter());
1411
+ registry.set("sui", () => new SuiAdapter());
1412
+ registry.set("aptos", () => new AptosAdapter());
1413
+ registry.set("ton", () => new TonAdapter());
894
1414
  }
895
1415
  });
896
1416
 
@@ -902,12 +1422,12 @@ function initMLClient(url) {
902
1422
  function getMLClient() {
903
1423
  return mlClient;
904
1424
  }
905
- var log, MLClient, mlClient;
1425
+ var log6, MLClient, mlClient;
906
1426
  var init_client = __esm({
907
1427
  "src/ml/client.ts"() {
908
1428
  "use strict";
909
1429
  init_logger();
910
- log = createLogger("ml-client");
1430
+ log6 = createLogger("ml-client");
911
1431
  MLClient = class {
912
1432
  baseUrl;
913
1433
  healthy = false;
@@ -925,7 +1445,7 @@ var init_client = __esm({
925
1445
  if (!res.ok) return null;
926
1446
  return await res.json();
927
1447
  } catch (err) {
928
- log.debug(`ML predict failed: ${err instanceof Error ? err.message : String(err)}`);
1448
+ log6.debug(`ML predict failed: ${err instanceof Error ? err.message : String(err)}`);
929
1449
  return null;
930
1450
  }
931
1451
  }
@@ -941,7 +1461,7 @@ var init_client = __esm({
941
1461
  const data = await res.json();
942
1462
  return data.predictions;
943
1463
  } catch (err) {
944
- log.debug(`ML batch predict failed: ${err instanceof Error ? err.message : String(err)}`);
1464
+ log6.debug(`ML batch predict failed: ${err instanceof Error ? err.message : String(err)}`);
945
1465
  return [];
946
1466
  }
947
1467
  }
@@ -957,7 +1477,7 @@ var init_client = __esm({
957
1477
  const data = await res.json();
958
1478
  return data.anomalies;
959
1479
  } catch (err) {
960
- log.debug(`ML anomaly detection failed: ${err instanceof Error ? err.message : String(err)}`);
1480
+ log6.debug(`ML anomaly detection failed: ${err instanceof Error ? err.message : String(err)}`);
961
1481
  return [];
962
1482
  }
963
1483
  }
@@ -984,7 +1504,7 @@ var init_client = __esm({
984
1504
  if (!res.ok) return null;
985
1505
  return await res.json();
986
1506
  } catch (err) {
987
- log.debug(`ML rug predict failed: ${err instanceof Error ? err.message : String(err)}`);
1507
+ log6.debug(`ML rug predict failed: ${err instanceof Error ? err.message : String(err)}`);
988
1508
  return null;
989
1509
  }
990
1510
  }
@@ -999,7 +1519,7 @@ var init_client = __esm({
999
1519
  if (!res.ok) return null;
1000
1520
  return await res.json();
1001
1521
  } catch (err) {
1002
- log.debug(`ML wallet classify failed: ${err instanceof Error ? err.message : String(err)}`);
1522
+ log6.debug(`ML wallet classify failed: ${err instanceof Error ? err.message : String(err)}`);
1003
1523
  return null;
1004
1524
  }
1005
1525
  }
@@ -1014,7 +1534,7 @@ var init_client = __esm({
1014
1534
  if (!res.ok) return null;
1015
1535
  return await res.json();
1016
1536
  } catch (err) {
1017
- log.debug(`ML sentiment failed: ${err instanceof Error ? err.message : String(err)}`);
1537
+ log6.debug(`ML sentiment failed: ${err instanceof Error ? err.message : String(err)}`);
1018
1538
  return null;
1019
1539
  }
1020
1540
  }
@@ -1030,7 +1550,7 @@ var init_client = __esm({
1030
1550
  const data = await res.json();
1031
1551
  return data.results;
1032
1552
  } catch (err) {
1033
- log.debug(`ML sentiment batch failed: ${err instanceof Error ? err.message : String(err)}`);
1553
+ log6.debug(`ML sentiment batch failed: ${err instanceof Error ? err.message : String(err)}`);
1034
1554
  return [];
1035
1555
  }
1036
1556
  }
@@ -1048,7 +1568,7 @@ var init_client = __esm({
1048
1568
  if (!res.ok) return null;
1049
1569
  return await res.json();
1050
1570
  } catch (err) {
1051
- log.debug(`ML trend score failed: ${err instanceof Error ? err.message : String(err)}`);
1571
+ log6.debug(`ML trend score failed: ${err instanceof Error ? err.message : String(err)}`);
1052
1572
  return null;
1053
1573
  }
1054
1574
  }
@@ -1063,7 +1583,7 @@ var init_client = __esm({
1063
1583
  if (!res.ok) return null;
1064
1584
  return await res.json();
1065
1585
  } catch (err) {
1066
- log.debug(`ML TA interpret failed: ${err instanceof Error ? err.message : String(err)}`);
1586
+ log6.debug(`ML TA interpret failed: ${err instanceof Error ? err.message : String(err)}`);
1067
1587
  return null;
1068
1588
  }
1069
1589
  }
@@ -1078,7 +1598,7 @@ var init_client = __esm({
1078
1598
  if (!res.ok) return null;
1079
1599
  return await res.json();
1080
1600
  } catch (err) {
1081
- log.debug(`ML strategy eval failed: ${err instanceof Error ? err.message : String(err)}`);
1601
+ log6.debug(`ML strategy eval failed: ${err instanceof Error ? err.message : String(err)}`);
1082
1602
  return null;
1083
1603
  }
1084
1604
  }
@@ -1093,7 +1613,7 @@ var init_client = __esm({
1093
1613
  if (!res.ok) return null;
1094
1614
  return await res.json();
1095
1615
  } catch (err) {
1096
- log.debug(`ML regime detect failed: ${err instanceof Error ? err.message : String(err)}`);
1616
+ log6.debug(`ML regime detect failed: ${err instanceof Error ? err.message : String(err)}`);
1097
1617
  return null;
1098
1618
  }
1099
1619
  }
@@ -1108,7 +1628,7 @@ var init_client = __esm({
1108
1628
  if (!res.ok) return null;
1109
1629
  return await res.json();
1110
1630
  } catch (err) {
1111
- log.debug(`ML project risk failed: ${err instanceof Error ? err.message : String(err)}`);
1631
+ log6.debug(`ML project risk failed: ${err instanceof Error ? err.message : String(err)}`);
1112
1632
  return null;
1113
1633
  }
1114
1634
  }
@@ -1123,7 +1643,7 @@ var init_client = __esm({
1123
1643
  if (!res.ok) return null;
1124
1644
  return await res.json();
1125
1645
  } catch (err) {
1126
- log.debug(`ML portfolio opt failed: ${err instanceof Error ? err.message : String(err)}`);
1646
+ log6.debug(`ML portfolio opt failed: ${err instanceof Error ? err.message : String(err)}`);
1127
1647
  return null;
1128
1648
  }
1129
1649
  }
@@ -1132,43 +1652,74 @@ var init_client = __esm({
1132
1652
  const res = await fetch(`${this.baseUrl}/predict/intent`, {
1133
1653
  method: "POST",
1134
1654
  headers: { "Content-Type": "application/json" },
1135
- body: JSON.stringify({ text }),
1655
+ body: JSON.stringify({ text }),
1656
+ signal: AbortSignal.timeout(5e3)
1657
+ });
1658
+ if (!res.ok) return null;
1659
+ return await res.json();
1660
+ } catch (err) {
1661
+ log6.debug(`ML intent classify failed: ${err instanceof Error ? err.message : String(err)}`);
1662
+ return null;
1663
+ }
1664
+ }
1665
+ async scoreBytecodeRisk(features) {
1666
+ try {
1667
+ const res = await fetch(`${this.baseUrl}/predict/bytecode-risk`, {
1668
+ method: "POST",
1669
+ headers: { "Content-Type": "application/json" },
1670
+ body: JSON.stringify(features),
1671
+ signal: AbortSignal.timeout(5e3)
1672
+ });
1673
+ if (!res.ok) return null;
1674
+ return await res.json();
1675
+ } catch (err) {
1676
+ log6.debug(`ML bytecode risk failed: ${err instanceof Error ? err.message : String(err)}`);
1677
+ return null;
1678
+ }
1679
+ }
1680
+ async predictPortfolioForward(features) {
1681
+ try {
1682
+ const res = await fetch(`${this.baseUrl}/predict/portfolio-forward`, {
1683
+ method: "POST",
1684
+ headers: { "Content-Type": "application/json" },
1685
+ body: JSON.stringify(features),
1136
1686
  signal: AbortSignal.timeout(5e3)
1137
1687
  });
1138
1688
  if (!res.ok) return null;
1139
1689
  return await res.json();
1140
1690
  } catch (err) {
1141
- log.debug(`ML intent classify failed: ${err instanceof Error ? err.message : String(err)}`);
1691
+ log6.debug(`ML portfolio forward failed: ${err instanceof Error ? err.message : String(err)}`);
1142
1692
  return null;
1143
1693
  }
1144
1694
  }
1145
- async scoreBytecodeRisk(features) {
1695
+ async trainModel(modelName, params) {
1146
1696
  try {
1147
- const res = await fetch(`${this.baseUrl}/predict/bytecode-risk`, {
1697
+ const res = await fetch(`${this.baseUrl}/train`, {
1148
1698
  method: "POST",
1149
1699
  headers: { "Content-Type": "application/json" },
1150
- body: JSON.stringify(features),
1151
- signal: AbortSignal.timeout(5e3)
1700
+ body: JSON.stringify({ model: modelName, ...params }),
1701
+ signal: AbortSignal.timeout(3e5)
1702
+ // 5 min for training
1152
1703
  });
1153
1704
  if (!res.ok) return null;
1154
1705
  return await res.json();
1155
1706
  } catch (err) {
1156
- log.debug(`ML bytecode risk failed: ${err instanceof Error ? err.message : String(err)}`);
1707
+ log6.debug(`ML train failed: ${err instanceof Error ? err.message : String(err)}`);
1157
1708
  return null;
1158
1709
  }
1159
1710
  }
1160
- async predictPortfolioForward(features) {
1711
+ async evaluateModel(modelName) {
1161
1712
  try {
1162
- const res = await fetch(`${this.baseUrl}/predict/portfolio-forward`, {
1713
+ const res = await fetch(`${this.baseUrl}/evaluate`, {
1163
1714
  method: "POST",
1164
1715
  headers: { "Content-Type": "application/json" },
1165
- body: JSON.stringify(features),
1166
- signal: AbortSignal.timeout(5e3)
1716
+ body: JSON.stringify({ model: modelName }),
1717
+ signal: AbortSignal.timeout(6e4)
1167
1718
  });
1168
1719
  if (!res.ok) return null;
1169
1720
  return await res.json();
1170
1721
  } catch (err) {
1171
- log.debug(`ML portfolio forward failed: ${err instanceof Error ? err.message : String(err)}`);
1722
+ log6.debug(`ML evaluate failed: ${err instanceof Error ? err.message : String(err)}`);
1172
1723
  return null;
1173
1724
  }
1174
1725
  }
@@ -1342,6 +1893,7 @@ __export(scan_exports, {
1342
1893
  import chalk from "chalk";
1343
1894
  import ora from "ora";
1344
1895
  async function handleScan(project, options) {
1896
+ assertValidAddress(project);
1345
1897
  const spinner = ora(`Scanning ${project} on ${options.chain}...`).start();
1346
1898
  try {
1347
1899
  const adapter = getAdapter(options.chain);
@@ -1396,6 +1948,7 @@ var init_scan = __esm({
1396
1948
  init_loader();
1397
1949
  init_project_analyzer();
1398
1950
  init_risk_scorer();
1951
+ init_validate();
1399
1952
  }
1400
1953
  });
1401
1954
 
@@ -1560,9 +2113,18 @@ async function fetchCryptoNews(symbol, apiToken) {
1560
2113
  const url = `${BASE_URL2}/posts/?${params.toString()}`;
1561
2114
  const res = await fetch(url);
1562
2115
  if (!res.ok) {
1563
- throw new Error(`CryptoPanic API error: ${res.status} ${res.statusText}`);
2116
+ return [];
2117
+ }
2118
+ const contentType = res.headers.get("content-type") ?? "";
2119
+ if (!contentType.includes("application/json")) {
2120
+ return [];
2121
+ }
2122
+ let data;
2123
+ try {
2124
+ data = await res.json();
2125
+ } catch {
2126
+ return [];
1564
2127
  }
1565
- const data = await res.json();
1566
2128
  const posts = data.results ?? [];
1567
2129
  return posts.map((post) => {
1568
2130
  const { positive, negative } = post.votes;
@@ -1718,7 +2280,7 @@ async function fetchJson2(url) {
1718
2280
  }
1719
2281
  async function fetchTickerPrice(symbol) {
1720
2282
  const pair = resolvePair(symbol);
1721
- const data = await fetchJson2(`${BASE_URL3}/ticker/24hr?symbol=${pair}`);
2283
+ const data = await fetchJson2(`${BASE_URL3}/ticker/24hr?symbol=${encodeURIComponent(pair)}`);
1722
2284
  return {
1723
2285
  symbol: symbol.toUpperCase(),
1724
2286
  price: parseFloat(data.lastPrice),
@@ -1728,7 +2290,7 @@ async function fetchTickerPrice(symbol) {
1728
2290
  async function fetchKlines(symbol, interval, limit = 100) {
1729
2291
  const pair = resolvePair(symbol);
1730
2292
  const data = await fetchJson2(
1731
- `${BASE_URL3}/klines?symbol=${pair}&interval=${interval}&limit=${limit}`
2293
+ `${BASE_URL3}/klines?symbol=${encodeURIComponent(pair)}&interval=${interval}&limit=${limit}`
1732
2294
  );
1733
2295
  return data.map((k) => ({
1734
2296
  openTime: Number(k[0]),
@@ -1744,7 +2306,7 @@ async function fetchKlines(symbol, interval, limit = 100) {
1744
2306
  }
1745
2307
  async function fetchFundingRate(symbol) {
1746
2308
  const pair = resolvePair(symbol);
1747
- const data = await fetchJson2(`${FUTURES_URL}/premiumIndex?symbol=${pair}`);
2309
+ const data = await fetchJson2(`${FUTURES_URL}/premiumIndex?symbol=${encodeURIComponent(pair)}`);
1748
2310
  const item = data[0];
1749
2311
  if (!item) throw new Error(`No funding data for ${pair}`);
1750
2312
  return {
@@ -1758,9 +2320,11 @@ async function fetchOpenInterest(symbol) {
1758
2320
  const pair = resolvePair(symbol);
1759
2321
  const [oiData, tickerData] = await Promise.all([
1760
2322
  fetchJson2(
1761
- `${FUTURES_URL}/openInterest?symbol=${pair}`
2323
+ `${FUTURES_URL}/openInterest?symbol=${encodeURIComponent(pair)}`
1762
2324
  ),
1763
- fetchJson2(`${FUTURES_URL}/ticker/price?symbol=${pair}`)
2325
+ fetchJson2(
2326
+ `${FUTURES_URL}/ticker/price?symbol=${encodeURIComponent(pair)}`
2327
+ )
1764
2328
  ]);
1765
2329
  const oi = parseFloat(oiData.openInterest);
1766
2330
  const price = parseFloat(tickerData.lastPrice);
@@ -1790,7 +2354,7 @@ async function fetchTopGainersLosers(limit = 10) {
1790
2354
  async function isValidSymbol(symbol) {
1791
2355
  try {
1792
2356
  const pair = resolvePair(symbol);
1793
- await fetchJson2(`${BASE_URL3}/ticker/price?symbol=${pair}`);
2357
+ await fetchJson2(`${BASE_URL3}/ticker/price?symbol=${encodeURIComponent(pair)}`);
1794
2358
  return true;
1795
2359
  } catch {
1796
2360
  return false;
@@ -2811,6 +3375,7 @@ __export(track_exports, {
2811
3375
  import chalk3 from "chalk";
2812
3376
  import ora3 from "ora";
2813
3377
  async function handleTrack(wallet, options) {
3378
+ assertValidAddress(wallet);
2814
3379
  const spinner = ora3(`Analyzing wallet ${wallet.slice(0, 10)}... on ${options.chain}`).start();
2815
3380
  try {
2816
3381
  const adapter = getAdapter(options.chain);
@@ -2871,6 +3436,7 @@ var init_track = __esm({
2871
3436
  init_registry();
2872
3437
  init_loader();
2873
3438
  init_wallet_analyzer();
3439
+ init_validate();
2874
3440
  }
2875
3441
  });
2876
3442
 
@@ -3163,6 +3729,7 @@ __export(audit_exports, {
3163
3729
  import chalk5 from "chalk";
3164
3730
  import ora5 from "ora";
3165
3731
  async function handleAudit(contract, options) {
3732
+ assertValidAddress(contract);
3166
3733
  const spinner = ora5(`Auditing contract ${contract.slice(0, 10)}... on ${options.chain}`).start();
3167
3734
  try {
3168
3735
  const adapter = getAdapter(options.chain);
@@ -3240,6 +3807,7 @@ var init_audit = __esm({
3240
3807
  init_registry();
3241
3808
  init_loader();
3242
3809
  init_contract_auditor();
3810
+ init_validate();
3243
3811
  }
3244
3812
  });
3245
3813
 
@@ -4292,7 +4860,9 @@ async function fetchJson6(url) {
4292
4860
  }
4293
4861
  async function checkTokenSecurity(contractAddress, chain) {
4294
4862
  const chainId = resolveChainId(chain);
4295
- const data = await fetchJson6(`${BASE_URL6}/token_security/${chainId}?contract_addresses=${contractAddress.toLowerCase()}`);
4863
+ const data = await fetchJson6(
4864
+ `${BASE_URL6}/token_security/${encodeURIComponent(chainId)}?contract_addresses=${encodeURIComponent(contractAddress.toLowerCase())}`
4865
+ );
4296
4866
  if (data.code !== 1) return null;
4297
4867
  const addr = contractAddress.toLowerCase();
4298
4868
  const raw = data.result[addr];
@@ -4347,7 +4917,9 @@ async function checkTokenSecurity(contractAddress, chain) {
4347
4917
  }
4348
4918
  async function checkAddressSecurity(address, chain) {
4349
4919
  const chainId = resolveChainId(chain);
4350
- const data = await fetchJson6(`${BASE_URL6}/address_security/${chainId}?address=${address.toLowerCase()}`);
4920
+ const data = await fetchJson6(
4921
+ `${BASE_URL6}/address_security/${encodeURIComponent(chainId)}?address=${encodeURIComponent(address.toLowerCase())}`
4922
+ );
4351
4923
  if (data.code !== 1) return null;
4352
4924
  const raw = data.result;
4353
4925
  const toBool = (v) => v === "1" || v === 1 || v === true;
@@ -4374,7 +4946,81 @@ var init_goplus = __esm({
4374
4946
  optimism: "10",
4375
4947
  base: "8453",
4376
4948
  avalanche: "43114",
4377
- solana: "solana"
4949
+ solana: "solana",
4950
+ sui: "sui",
4951
+ aptos: "aptos",
4952
+ ton: "ton"
4953
+ };
4954
+ }
4955
+ });
4956
+
4957
+ // src/ai/sanitize.ts
4958
+ function sanitizeExternalData(text, maxLen = LIMITS.generic) {
4959
+ let cleaned = text;
4960
+ for (const pattern of INJECTION_PATTERNS) {
4961
+ cleaned = cleaned.replace(pattern, "[FILTERED]");
4962
+ }
4963
+ cleaned = cleaned.replace(/^#{1,6}\s+/gm, "");
4964
+ cleaned = cleaned.replace(/```[\s\S]*?```/g, "[CODE]");
4965
+ if (cleaned.length > maxLen) {
4966
+ cleaned = cleaned.slice(0, maxLen) + "...";
4967
+ }
4968
+ return cleaned;
4969
+ }
4970
+ function sanitizeTokenName(name) {
4971
+ return sanitizeExternalData(name, LIMITS.tokenName);
4972
+ }
4973
+ function sanitizeHeadline(headline) {
4974
+ return sanitizeExternalData(headline, LIMITS.headline);
4975
+ }
4976
+ function wrapUntrustedData(label, data) {
4977
+ return `[BEGIN EXTERNAL DATA: ${label}]
4978
+ ${data}
4979
+ [END EXTERNAL DATA: ${label}]`;
4980
+ }
4981
+ function sanitizeToolResult(result) {
4982
+ if (typeof result === "string") {
4983
+ return sanitizeExternalData(result);
4984
+ }
4985
+ if (Array.isArray(result)) {
4986
+ return result.map(sanitizeToolResult);
4987
+ }
4988
+ if (result !== null && typeof result === "object") {
4989
+ const sanitized = {};
4990
+ for (const [key, value] of Object.entries(result)) {
4991
+ sanitized[key] = sanitizeToolResult(value);
4992
+ }
4993
+ return sanitized;
4994
+ }
4995
+ return result;
4996
+ }
4997
+ var INJECTION_PATTERNS, LIMITS;
4998
+ var init_sanitize = __esm({
4999
+ "src/ai/sanitize.ts"() {
5000
+ "use strict";
5001
+ INJECTION_PATTERNS = [
5002
+ /ignore\s+(all\s+)?previous\s+instructions/gi,
5003
+ /ignore\s+(all\s+)?above/gi,
5004
+ /you\s+are\s+now/gi,
5005
+ /system\s*:/gi,
5006
+ /assistant\s*:/gi,
5007
+ /\[system\]/gi,
5008
+ /\[instruction\]/gi,
5009
+ /<\/?system>/gi,
5010
+ /repeat\s+(everything|the\s+prompt|your\s+instructions)/gi,
5011
+ /reveal\s+(your|the)\s+(prompt|instructions|config)/gi,
5012
+ /forget\s+(everything|your\s+instructions)/gi,
5013
+ /override\s+(previous|your|all)/gi,
5014
+ /new\s+instructions?\s*:/gi,
5015
+ /\bdo\s+not\s+follow\b/gi,
5016
+ /\bact\s+as\b/gi,
5017
+ /\brole\s*:\s*(system|assistant)\b/gi
5018
+ ];
5019
+ LIMITS = {
5020
+ tokenName: 60,
5021
+ headline: 250,
5022
+ description: 500,
5023
+ generic: 1e3
4378
5024
  };
4379
5025
  }
4380
5026
  });
@@ -4601,10 +5247,11 @@ async function buildContextBlock(userMessage) {
4601
5247
  }
4602
5248
  output.push(...buildInstructions(queryType, hasSpecificToken, isComplexQuery));
4603
5249
  output.push("");
4604
- return output.join("\n");
5250
+ const raw = output.join("\n");
5251
+ return wrapUntrustedData("MARKET_CONTEXT", raw);
4605
5252
  }
4606
5253
  function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
4607
- const base2 = [
5254
+ const base3 = [
4608
5255
  "CRITICAL INSTRUCTIONS (MUST FOLLOW):",
4609
5256
  '- NEVER output "--- REAL-TIME DATA ---" or "--- END REAL-TIME DATA ---" markers.',
4610
5257
  "- You MUST use ONLY the real-time data above. Do NOT invent or fabricate any data.",
@@ -4613,7 +5260,7 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
4613
5260
  ];
4614
5261
  switch (queryType) {
4615
5262
  case "news":
4616
- base2.push(
5263
+ base3.push(
4617
5264
  "",
4618
5265
  "QUERY TYPE: NEWS \u2014 The user wants crypto news and market updates.",
4619
5266
  "- Summarize the news headlines from the data above.",
@@ -4624,7 +5271,7 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
4624
5271
  );
4625
5272
  break;
4626
5273
  case "trends":
4627
- base2.push(
5274
+ base3.push(
4628
5275
  "",
4629
5276
  "QUERY TYPE: TRENDS/MARKET OVERVIEW \u2014 The user wants to know what is trending.",
4630
5277
  "- Lead with the overall market sentiment (Fear & Greed + BTC direction).",
@@ -4635,7 +5282,7 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
4635
5282
  );
4636
5283
  break;
4637
5284
  case "token_analysis":
4638
- base2.push(
5285
+ base3.push(
4639
5286
  "",
4640
5287
  "QUERY TYPE: TOKEN ANALYSIS \u2014 The user wants deep analysis of a specific token.",
4641
5288
  "- Follow the analysis report structure above (Verdict \u2192 Market \u2192 Security \u2192 Signals \u2192 Risks).",
@@ -4646,7 +5293,7 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
4646
5293
  );
4647
5294
  break;
4648
5295
  case "prediction":
4649
- base2.push(
5296
+ base3.push(
4650
5297
  "",
4651
5298
  "QUERY TYPE: PRICE PREDICTION \u2014 The user wants future price projections.",
4652
5299
  "- ALWAYS include the PRICE PREDICTION SCENARIOS with the exact dollar values from the data.",
@@ -4659,7 +5306,7 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
4659
5306
  );
4660
5307
  break;
4661
5308
  default:
4662
- base2.push(
5309
+ base3.push(
4663
5310
  "",
4664
5311
  "QUERY TYPE: GENERAL \u2014 Answer naturally using the data above as context.",
4665
5312
  "- Use the real-time prices and sentiment as background context.",
@@ -4670,12 +5317,12 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
4670
5317
  break;
4671
5318
  }
4672
5319
  if (isComplexQuery && hasSpecificToken) {
4673
- base2.push(
5320
+ base3.push(
4674
5321
  "",
4675
5322
  "COMPLEX ANALYSIS: Structure your reasoning as Data \u2192 Signals \u2192 Alignment \u2192 Confidence \u2192 Risks \u2192 Conclusion."
4676
5323
  );
4677
5324
  }
4678
- return base2;
5325
+ return base3;
4679
5326
  }
4680
5327
  async function fetchTokenData(tokens, address) {
4681
5328
  const lines = ["## Token / Price Data"];
@@ -4685,7 +5332,7 @@ async function fetchTokenData(tokens, address) {
4685
5332
  const pair = pairs[0];
4686
5333
  if (pair) {
4687
5334
  lines.push(
4688
- `${pair.baseToken.name} (${pair.baseToken.symbol}) on ${pair.chainId}:`,
5335
+ `${sanitizeTokenName(pair.baseToken.name)} (${sanitizeTokenName(pair.baseToken.symbol)}) on ${pair.chainId}:`,
4689
5336
  ` Price: $${pair.priceUsd ?? "?"}`,
4690
5337
  ` 24h Volume: $${(pair.volume?.h24 ?? 0).toLocaleString()}`,
4691
5338
  ` Liquidity: $${(pair.liquidity?.usd ?? 0).toLocaleString()}`,
@@ -4754,7 +5401,7 @@ async function fetchTokenData(tokens, address) {
4754
5401
  const pair = pairs[0];
4755
5402
  if (pair) {
4756
5403
  lines.push(
4757
- `${pair.baseToken.name} (${pair.baseToken.symbol}) on ${pair.chainId}:`,
5404
+ `${sanitizeTokenName(pair.baseToken.name)} (${sanitizeTokenName(pair.baseToken.symbol)}) on ${pair.chainId}:`,
4758
5405
  ` Price: $${pair.priceUsd ?? "?"}`,
4759
5406
  ` 24h Volume: $${(pair.volume?.h24 ?? 0).toLocaleString()}`,
4760
5407
  ` 24h Change: ${(pair.priceChange?.h24 ?? 0) > 0 ? "+" : ""}${(pair.priceChange?.h24 ?? 0).toFixed(2)}%`,
@@ -4773,7 +5420,7 @@ async function fetchTrendingData() {
4773
5420
  const lines = ["## Trending Tokens (live)"];
4774
5421
  for (const t of trending.slice(0, 10)) {
4775
5422
  lines.push(
4776
- `- ${t.name} (${t.symbol}) on ${t.chain}: $${t.priceUsd} | 24h: ${t.priceChange24h > 0 ? "+" : ""}${t.priceChange24h.toFixed(1)}% | Vol: $${t.volume24h.toLocaleString()} [${t.source}]`
5423
+ `- ${sanitizeTokenName(t.name)} (${sanitizeTokenName(t.symbol)}) on ${t.chain}: $${t.priceUsd} | 24h: ${t.priceChange24h > 0 ? "+" : ""}${t.priceChange24h.toFixed(1)}% | Vol: $${t.volume24h.toLocaleString()} [${t.source}]`
4777
5424
  );
4778
5425
  }
4779
5426
  return lines.join("\n");
@@ -4797,7 +5444,9 @@ async function fetchNewsData(symbol) {
4797
5444
  const n = headlines[i];
4798
5445
  const ml = mlResults[i];
4799
5446
  const label = ml ? `${ml.sentiment.toUpperCase()} (${(ml.confidence * 100).toFixed(0)}%)` : n.sentiment.toUpperCase();
4800
- lines.push(`- [${label}] ${n.title} (${n.source.title}, ${n.publishedAt})`);
5447
+ lines.push(
5448
+ `- [${label}] ${sanitizeHeadline(n.title)} (${sanitizeTokenName(n.source.title)}, ${n.publishedAt})`
5449
+ );
4801
5450
  }
4802
5451
  const avgScore = mlResults.reduce((s, r) => s + r.score, 0) / mlResults.length;
4803
5452
  const avgSentiment = avgScore > 0.2 ? "BULLISH" : avgScore < -0.2 ? "BEARISH" : "NEUTRAL";
@@ -4810,7 +5459,7 @@ ML Aggregate Sentiment: ${avgSentiment} (score: ${avgScore.toFixed(3)})`);
4810
5459
  }
4811
5460
  for (const n of headlines) {
4812
5461
  lines.push(
4813
- `- [${n.sentiment.toUpperCase()}] ${n.title} (${n.source.title}, ${n.publishedAt})`
5462
+ `- [${n.sentiment.toUpperCase()}] ${sanitizeHeadline(n.title)} (${sanitizeTokenName(n.source.title)}, ${n.publishedAt})`
4814
5463
  );
4815
5464
  }
4816
5465
  return lines.join("\n");
@@ -4850,7 +5499,7 @@ async function fetchRaisesData() {
4850
5499
  const amount = r.amount ? r.amount >= 1e9 ? `$${(r.amount / 1e9).toFixed(1)}B` : r.amount >= 1e6 ? `$${(r.amount / 1e6).toFixed(1)}M` : r.amount >= 1e3 ? `$${(r.amount / 1e3).toFixed(0)}K` : `$${r.amount.toLocaleString()}` : "undisclosed";
4851
5500
  const date = new Date(r.date * 1e3).toISOString().split("T")[0];
4852
5501
  lines.push(
4853
- `- ${r.name} \u2014 ${r.round} (${amount}) on ${r.chains.join(", ") || "multi-chain"} [${date}]${r.leadInvestors.length > 0 ? ` Led by: ${r.leadInvestors.join(", ")}` : ""}`
5502
+ `- ${sanitizeTokenName(r.name)} \u2014 ${sanitizeExternalData(r.round, 50)} (${amount}) on ${r.chains.join(", ") || "multi-chain"} [${date}]${r.leadInvestors.length > 0 ? ` Led by: ${r.leadInvestors.map((inv) => sanitizeTokenName(inv)).join(", ")}` : ""}`
4854
5503
  );
4855
5504
  }
4856
5505
  return lines.join("\n");
@@ -4865,7 +5514,9 @@ async function fetchPumpData() {
4865
5514
  const lines = ["## Latest Pump.fun Launches (Solana)"];
4866
5515
  for (const c of coins) {
4867
5516
  const mcap = c.usd_market_cap ? `$${c.usd_market_cap.toFixed(0)}` : "?";
4868
- lines.push(`- ${c.name} (${c.symbol}) \u2014 MC: ${mcap} | Replies: ${c.reply_count}`);
5517
+ lines.push(
5518
+ `- ${sanitizeTokenName(c.name)} (${sanitizeTokenName(c.symbol)}) \u2014 MC: ${mcap} | Replies: ${c.reply_count}`
5519
+ );
4869
5520
  }
4870
5521
  return lines.join("\n");
4871
5522
  } catch {
@@ -5785,6 +6436,7 @@ var init_context_injector = __esm({
5785
6436
  init_loader();
5786
6437
  init_constants();
5787
6438
  init_client();
6439
+ init_sanitize();
5788
6440
  PRICE_KEYWORDS = ["price", "worth", "cost", "value", "how much"];
5789
6441
  TRENDING_KEYWORDS = ["trending", "hot", "popular", "top", "best", "hype"];
5790
6442
  NEWS_KEYWORDS = ["news", "latest", "update", "happening", "announcement"];
@@ -6524,7 +7176,12 @@ function getCached(key) {
6524
7176
  getDb().prepare("DELETE FROM cache WHERE key = ?").run(key);
6525
7177
  return null;
6526
7178
  }
6527
- return JSON.parse(row.value);
7179
+ try {
7180
+ return JSON.parse(row.value);
7181
+ } catch {
7182
+ getDb().prepare("DELETE FROM cache WHERE key = ?").run(key);
7183
+ return null;
7184
+ }
6528
7185
  }
6529
7186
  function setCache(key, value, ttlSeconds) {
6530
7187
  const now = Math.floor(Date.now() / 1e3);
@@ -6556,12 +7213,14 @@ var init_engine = __esm({
6556
7213
  logger = createLogger("agent-engine");
6557
7214
  AgentEngine = class {
6558
7215
  timer = null;
7216
+ running = false;
6559
7217
  state;
6560
7218
  strategy;
6561
7219
  constructor(config2) {
6562
7220
  this.strategy = getStrategy(config2.strategy);
7221
+ const interval = Math.max(30, config2.interval);
6563
7222
  this.state = {
6564
- config: config2,
7223
+ config: { ...config2, interval },
6565
7224
  status: "idle",
6566
7225
  lastCycle: null,
6567
7226
  cycleCount: 0,
@@ -6575,25 +7234,35 @@ var init_engine = __esm({
6575
7234
  if (this.state.status === "running") return;
6576
7235
  this.state.status = "running";
6577
7236
  this.state.error = null;
7237
+ this.running = true;
6578
7238
  logger.info(`Agent ${this.state.config.name} started (${this.strategy.name})`);
6579
- void this.runCycle();
6580
- this.timer = setInterval(() => {
6581
- void this.runCycle();
6582
- }, this.state.config.interval * 1e3);
7239
+ void this.scheduleNextCycle(true);
6583
7240
  }
6584
7241
  stop() {
7242
+ this.running = false;
6585
7243
  if (this.timer) {
6586
- clearInterval(this.timer);
7244
+ clearTimeout(this.timer);
6587
7245
  this.timer = null;
6588
7246
  }
6589
7247
  this.state.status = "stopped";
6590
7248
  logger.info(`Agent ${this.state.config.name} stopped`);
6591
7249
  }
7250
+ async scheduleNextCycle(immediate = false) {
7251
+ if (!this.running) return;
7252
+ if (!immediate) {
7253
+ await new Promise((resolve4) => {
7254
+ this.timer = setTimeout(resolve4, this.state.config.interval * 1e3);
7255
+ });
7256
+ }
7257
+ if (!this.running) return;
7258
+ await this.runCycle();
7259
+ void this.scheduleNextCycle(false);
7260
+ }
6592
7261
  async runCycle() {
6593
7262
  for (const symbol of this.state.config.pairs) {
6594
7263
  try {
6595
7264
  const signals = await this.gatherSignals(symbol);
6596
- const decision = this.strategy.evaluate(signals);
7265
+ const decision = this.strategy.evaluateAsync ? await this.strategy.evaluateAsync(signals, symbol) : this.strategy.evaluate(signals);
6597
7266
  const result = {
6598
7267
  agentId: this.state.config.id,
6599
7268
  symbol,
@@ -6890,6 +7559,60 @@ function evaluateWithRules(signals) {
6890
7559
  reasoning: [...reasoning, "Mixed signals \u2014 holding"]
6891
7560
  };
6892
7561
  }
7562
+ async function evaluateWithML(symbol, signals) {
7563
+ const mlClient2 = getMLClient();
7564
+ if (!mlClient2) return evaluateWithRules(signals);
7565
+ try {
7566
+ const regime = await detectMarketRegime(symbol, {
7567
+ returns_1d: signals.priceChange24h ?? 0,
7568
+ returns_7d: 0,
7569
+ volatility_14d: signals.atr !== null && signals.price !== null && signals.price > 0 ? signals.atr / signals.price * 100 : 3,
7570
+ volume_ratio: 1,
7571
+ rsi: signals.rsi ?? 50,
7572
+ bb_width: 0,
7573
+ fear_greed: signals.fearGreed ?? 50,
7574
+ funding_rate: signals.fundingRate ?? 0,
7575
+ price_vs_sma200: 0
7576
+ });
7577
+ const strategyResult = await mlClient2.evaluateStrategy({
7578
+ rsi: signals.rsi ?? 50,
7579
+ macd_histogram: signals.macdHistogram ?? 0,
7580
+ ema12: signals.ema12 ?? 0,
7581
+ ema26: signals.ema26 ?? 0,
7582
+ bollinger_pct_b: signals.bollingerPercentB ?? 0.5,
7583
+ atr: signals.atr ?? 0,
7584
+ obv: signals.obv ?? 0,
7585
+ funding_rate: signals.fundingRate ?? 0,
7586
+ fear_greed: signals.fearGreed ?? 50,
7587
+ price_change_24h: signals.priceChange24h ?? 0,
7588
+ price: signals.price ?? 0,
7589
+ regime: regime.regime
7590
+ });
7591
+ if (strategyResult && strategyResult.confidence > 60) {
7592
+ return {
7593
+ action: strategyResult.action,
7594
+ confidence: Math.round(strategyResult.confidence),
7595
+ reasoning: [
7596
+ `Regime: ${regime.regime} (${regime.confidence.toFixed(0)}% confidence)`,
7597
+ ...strategyResult.reasoning
7598
+ ]
7599
+ };
7600
+ }
7601
+ const ruleDecision = evaluateWithRules(signals);
7602
+ return {
7603
+ action: ruleDecision.action,
7604
+ confidence: Math.round(((strategyResult?.confidence ?? 0) + ruleDecision.confidence) / 2),
7605
+ reasoning: [
7606
+ `Regime: ${regime.regime}`,
7607
+ ...strategyResult?.reasoning ?? [],
7608
+ "(Low ML confidence \u2014 blending with rules)",
7609
+ ...ruleDecision.reasoning
7610
+ ]
7611
+ };
7612
+ } catch {
7613
+ return evaluateWithRules(signals);
7614
+ }
7615
+ }
6893
7616
  var mlAdaptiveStrategy;
6894
7617
  var init_ml_adaptive = __esm({
6895
7618
  "src/core/agent/strategies/ml-adaptive.ts"() {
@@ -6901,6 +7624,9 @@ var init_ml_adaptive = __esm({
6901
7624
  description: "ML-powered strategy using contextual bandit approach. Uses LSTM/RF predictions when available, falls back to rule-based when < 100 training samples.",
6902
7625
  evaluate(signals) {
6903
7626
  return evaluateWithRules(signals);
7627
+ },
7628
+ async evaluateAsync(signals, symbol) {
7629
+ return evaluateWithML(symbol, signals);
6904
7630
  }
6905
7631
  };
6906
7632
  }
@@ -6948,6 +7674,10 @@ function ensureAgentTables() {
6948
7674
  function createAgent(name, strategy, pairs, interval = 60) {
6949
7675
  ensureAgentTables();
6950
7676
  getStrategy(strategy);
7677
+ const existing = getAgentByName(name);
7678
+ if (existing) {
7679
+ throw new Error(`Agent "${name}" already exists. Delete it first or choose a different name.`);
7680
+ }
6951
7681
  const id = randomUUID();
6952
7682
  const now = Date.now();
6953
7683
  getDb().prepare(
@@ -7004,24 +7734,32 @@ function deleteAgent(id) {
7004
7734
  engine.stop();
7005
7735
  engines.delete(id);
7006
7736
  }
7007
- const result = getDb().prepare("DELETE FROM agents WHERE id = ?").run(id);
7008
7737
  getDb().prepare("DELETE FROM agent_decisions WHERE agent_id = ?").run(id);
7738
+ const result = getDb().prepare("DELETE FROM agents WHERE id = ?").run(id);
7009
7739
  return result.changes > 0;
7010
7740
  }
7011
7741
  function startAgent(id) {
7012
7742
  const config2 = getAgentById(id);
7013
7743
  if (!config2) throw new Error(`Agent not found: ${id}`);
7014
- let engine = engines.get(id);
7015
- if (!engine) {
7016
- engine = new AgentEngine(config2);
7017
- engines.set(id, engine);
7744
+ const existing = engines.get(id);
7745
+ if (existing) {
7746
+ const state = existing.getState();
7747
+ if (state.status === "running") {
7748
+ return state;
7749
+ }
7018
7750
  }
7751
+ const engine = new AgentEngine(config2);
7752
+ engines.set(id, engine);
7019
7753
  engine.start();
7020
7754
  return engine.getState();
7021
7755
  }
7022
7756
  function stopAgent(id) {
7023
7757
  const engine = engines.get(id);
7024
- if (!engine) throw new Error(`Agent not running: ${id}`);
7758
+ if (!engine) {
7759
+ const config2 = getAgentById(id);
7760
+ if (!config2) throw new Error(`Agent not found: ${id}`);
7761
+ return { config: config2, status: "idle", lastCycle: null, cycleCount: 0, error: null };
7762
+ }
7025
7763
  engine.stop();
7026
7764
  return engine.getState();
7027
7765
  }
@@ -7052,23 +7790,43 @@ function logDecision(result) {
7052
7790
  JSON.stringify(result.signals),
7053
7791
  result.timestamp
7054
7792
  );
7793
+ pruneCounter++;
7794
+ if (pruneCounter >= 100) {
7795
+ pruneCounter = 0;
7796
+ pruneDecisions(result.agentId, 1e3);
7797
+ }
7798
+ }
7799
+ function pruneDecisions(agentId, keep = 1e3) {
7800
+ ensureAgentTables();
7801
+ const result = getDb().prepare(
7802
+ `DELETE FROM agent_decisions WHERE agent_id = ? AND id NOT IN (
7803
+ SELECT id FROM agent_decisions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?
7804
+ )`
7805
+ ).run(agentId, agentId, keep);
7806
+ return result.changes;
7055
7807
  }
7056
7808
  function getRecentDecisions(agentId, limit = 20) {
7057
7809
  ensureAgentTables();
7058
7810
  const rows = getDb().prepare(`SELECT * FROM agent_decisions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`).all(agentId, limit);
7059
- return rows.map((r) => ({
7060
- agentId: r.agent_id,
7061
- symbol: r.symbol,
7062
- timestamp: r.created_at,
7063
- signals: JSON.parse(r.signals),
7064
- decision: {
7065
- action: r.action,
7066
- confidence: r.confidence,
7067
- reasoning: JSON.parse(r.reasoning)
7811
+ return rows.map((r) => {
7812
+ try {
7813
+ return {
7814
+ agentId: r.agent_id,
7815
+ symbol: r.symbol,
7816
+ timestamp: r.created_at,
7817
+ signals: JSON.parse(r.signals),
7818
+ decision: {
7819
+ action: r.action,
7820
+ confidence: r.confidence,
7821
+ reasoning: JSON.parse(r.reasoning)
7822
+ }
7823
+ };
7824
+ } catch {
7825
+ return null;
7068
7826
  }
7069
- }));
7827
+ }).filter((r) => r !== null);
7070
7828
  }
7071
- var STRATEGIES, engines;
7829
+ var STRATEGIES, engines, pruneCounter;
7072
7830
  var init_manager = __esm({
7073
7831
  "src/core/agent/manager.ts"() {
7074
7832
  "use strict";
@@ -7083,12 +7841,13 @@ var init_manager = __esm({
7083
7841
  "ml-adaptive": mlAdaptiveStrategy
7084
7842
  };
7085
7843
  engines = /* @__PURE__ */ new Map();
7844
+ pruneCounter = 0;
7086
7845
  }
7087
7846
  });
7088
7847
 
7089
7848
  // src/core/agent/wallet.ts
7090
- import { createPublicClient as createPublicClient2, http as http2, formatEther } from "viem";
7091
- import { mainnet as mainnet2 } from "viem/chains";
7849
+ import { createPublicClient as createPublicClient2, http as http3, formatEther } from "viem";
7850
+ import { mainnet as mainnet3 } from "viem/chains";
7092
7851
  async function getWalletBalance(address) {
7093
7852
  const balance = await client.getBalance({ address });
7094
7853
  return formatEther(balance);
@@ -7101,8 +7860,8 @@ var init_wallet = __esm({
7101
7860
  "src/core/agent/wallet.ts"() {
7102
7861
  "use strict";
7103
7862
  client = createPublicClient2({
7104
- chain: mainnet2,
7105
- transport: http2()
7863
+ chain: mainnet3,
7864
+ transport: http3()
7106
7865
  });
7107
7866
  }
7108
7867
  });
@@ -7547,8 +8306,176 @@ var init_store_factory = __esm({
7547
8306
  }
7548
8307
  });
7549
8308
 
8309
+ // src/core/backtest/engine.ts
8310
+ var engine_exports = {};
8311
+ __export(engine_exports, {
8312
+ BacktestEngine: () => BacktestEngine
8313
+ });
8314
+ var log7, BacktestEngine;
8315
+ var init_engine2 = __esm({
8316
+ "src/core/backtest/engine.ts"() {
8317
+ "use strict";
8318
+ init_binance();
8319
+ init_manager();
8320
+ init_logger();
8321
+ log7 = createLogger("backtest");
8322
+ BacktestEngine = class {
8323
+ config;
8324
+ constructor(config2) {
8325
+ this.config = config2;
8326
+ }
8327
+ async run() {
8328
+ log7.info(
8329
+ `Backtest: ${this.config.strategy} on ${this.config.pair} (${this.config.from} \u2192 ${this.config.to})`
8330
+ );
8331
+ const strategy = getStrategy(this.config.strategy);
8332
+ const klines = await fetchKlines(this.config.pair, this.config.timeframe, 1e3);
8333
+ const fromTs = new Date(this.config.from).getTime();
8334
+ const toTs = new Date(this.config.to).getTime();
8335
+ const filtered = klines.filter((k) => k.openTime >= fromTs && k.openTime <= toTs);
8336
+ if (filtered.length === 0) {
8337
+ return this.emptyResult();
8338
+ }
8339
+ const trades = [];
8340
+ const equityCurve = [];
8341
+ const drawdownCurve = [];
8342
+ let cash = this.config.initialCapital;
8343
+ let position = null;
8344
+ let peak = cash;
8345
+ for (const candle of filtered) {
8346
+ const price = candle.close;
8347
+ const equity = position ? cash + position.size * price : cash;
8348
+ if (equity > peak) peak = equity;
8349
+ const drawdown = peak > 0 ? (peak - equity) / peak * 100 : 0;
8350
+ equityCurve.push({ time: candle.openTime, equity });
8351
+ drawdownCurve.push({ time: candle.openTime, drawdown });
8352
+ const signals = {
8353
+ rsi: null,
8354
+ macdHistogram: null,
8355
+ ema12: null,
8356
+ ema26: null,
8357
+ bollingerPercentB: null,
8358
+ atr: null,
8359
+ obv: null,
8360
+ fundingRate: null,
8361
+ fearGreed: null,
8362
+ priceChange24h: null,
8363
+ price
8364
+ };
8365
+ const decision = strategy.evaluate(signals);
8366
+ if (decision.action === "buy" && !position && cash > 0) {
8367
+ const size = cash * 0.95 / price;
8368
+ const commission = cash * 0.95 * (this.config.commissionPct / 100);
8369
+ cash -= size * price + commission;
8370
+ position = { side: "long", entryPrice: price, entryTime: candle.openTime, size };
8371
+ } else if (decision.action === "sell" && position) {
8372
+ const exitValue = position.size * price;
8373
+ const commission = exitValue * (this.config.commissionPct / 100);
8374
+ const pnl = exitValue - position.size * position.entryPrice - commission;
8375
+ const pnlPct = pnl / (position.size * position.entryPrice) * 100;
8376
+ trades.push({
8377
+ entryTime: position.entryTime,
8378
+ exitTime: candle.openTime,
8379
+ side: position.side,
8380
+ entryPrice: position.entryPrice,
8381
+ exitPrice: price,
8382
+ size: position.size,
8383
+ pnl,
8384
+ pnlPct,
8385
+ reason: decision.reasoning.join("; ")
8386
+ });
8387
+ cash += exitValue - commission;
8388
+ position = null;
8389
+ }
8390
+ }
8391
+ if (position && filtered.length > 0) {
8392
+ const lastPrice = filtered[filtered.length - 1].close;
8393
+ const exitValue = position.size * lastPrice;
8394
+ const pnl = exitValue - position.size * position.entryPrice;
8395
+ trades.push({
8396
+ entryTime: position.entryTime,
8397
+ exitTime: filtered[filtered.length - 1].openTime,
8398
+ side: position.side,
8399
+ entryPrice: position.entryPrice,
8400
+ exitPrice: lastPrice,
8401
+ size: position.size,
8402
+ pnl,
8403
+ pnlPct: pnl / (position.size * position.entryPrice) * 100,
8404
+ reason: "End of backtest period"
8405
+ });
8406
+ cash += exitValue;
8407
+ }
8408
+ const metrics = this.calculateMetrics(trades, cash);
8409
+ return {
8410
+ config: this.config,
8411
+ trades,
8412
+ metrics,
8413
+ equityCurve,
8414
+ drawdownCurve
8415
+ };
8416
+ }
8417
+ calculateMetrics(trades, finalCash) {
8418
+ const wins = trades.filter((t) => t.pnl > 0);
8419
+ const losses = trades.filter((t) => t.pnl <= 0);
8420
+ const totalReturn = finalCash - this.config.initialCapital;
8421
+ const totalReturnPct = totalReturn / this.config.initialCapital * 100;
8422
+ const winRate = trades.length > 0 ? wins.length / trades.length : 0;
8423
+ const grossProfit = wins.reduce((sum, t) => sum + t.pnl, 0);
8424
+ const grossLoss = Math.abs(losses.reduce((sum, t) => sum + t.pnl, 0));
8425
+ const profitFactor = grossLoss > 0 ? grossProfit / grossLoss : grossProfit > 0 ? Infinity : 0;
8426
+ const returns = trades.map((t) => t.pnlPct / 100);
8427
+ const avgReturn = returns.length > 0 ? returns.reduce((a, b) => a + b, 0) / returns.length : 0;
8428
+ const variance = returns.length > 1 ? returns.reduce((s, r) => s + (r - avgReturn) ** 2, 0) / (returns.length - 1) : 0;
8429
+ const sharpeRatio = Math.sqrt(variance) > 0 ? avgReturn / Math.sqrt(variance) * Math.sqrt(365) : 0;
8430
+ let maxDrawdown = 0;
8431
+ let peak = this.config.initialCapital;
8432
+ let running = this.config.initialCapital;
8433
+ for (const t of trades) {
8434
+ running += t.pnl;
8435
+ if (running > peak) peak = running;
8436
+ const dd = peak > 0 ? (peak - running) / peak * 100 : 0;
8437
+ if (dd > maxDrawdown) maxDrawdown = dd;
8438
+ }
8439
+ const avgHoldingPeriodMs = trades.length > 0 ? trades.reduce((sum, t) => sum + (t.exitTime - t.entryTime), 0) / trades.length : 0;
8440
+ return {
8441
+ totalReturn,
8442
+ totalReturnPct,
8443
+ winRate,
8444
+ totalTrades: trades.length,
8445
+ profitFactor,
8446
+ sharpeRatio,
8447
+ maxDrawdown,
8448
+ avgHoldingPeriodMs
8449
+ };
8450
+ }
8451
+ emptyResult() {
8452
+ return {
8453
+ config: this.config,
8454
+ trades: [],
8455
+ metrics: {
8456
+ totalReturn: 0,
8457
+ totalReturnPct: 0,
8458
+ winRate: 0,
8459
+ totalTrades: 0,
8460
+ profitFactor: 0,
8461
+ sharpeRatio: 0,
8462
+ maxDrawdown: 0,
8463
+ avgHoldingPeriodMs: 0
8464
+ },
8465
+ equityCurve: [],
8466
+ drawdownCurve: []
8467
+ };
8468
+ }
8469
+ };
8470
+ }
8471
+ });
8472
+
7550
8473
  // src/ai/tool-handler.ts
7551
8474
  async function handleTool(name, input) {
8475
+ const raw = await handleToolUnsafe(name, input);
8476
+ return sanitizeToolResult(raw);
8477
+ }
8478
+ async function handleToolUnsafe(name, input) {
7552
8479
  const params = input;
7553
8480
  switch (name) {
7554
8481
  case "get_token_info": {
@@ -8220,6 +9147,92 @@ async function handleTool(name, input) {
8220
9147
  }))
8221
9148
  };
8222
9149
  }
9150
+ case "get_ml_model_health": {
9151
+ let mlClient2 = getMLClient();
9152
+ if (!mlClient2) {
9153
+ try {
9154
+ const cfg = getConfig();
9155
+ if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
9156
+ mlClient2 = initMLClient(cfg.ml.sidecarUrl);
9157
+ }
9158
+ } catch {
9159
+ }
9160
+ }
9161
+ if (!mlClient2) {
9162
+ return { error: "ML sidecar not configured" };
9163
+ }
9164
+ const health = await mlClient2.getModelHealth();
9165
+ if (!health) {
9166
+ return { error: "ML sidecar not responding" };
9167
+ }
9168
+ return {
9169
+ models: health.models.map((m) => ({
9170
+ name: m.name,
9171
+ version: m.version,
9172
+ loaded: m.loaded,
9173
+ lastTrained: m.lastTrained,
9174
+ accuracy: m.accuracy
9175
+ })),
9176
+ uptime: health.uptime,
9177
+ predictionsServed: health.predictionsServed
9178
+ };
9179
+ }
9180
+ case "classify_user_intent": {
9181
+ const text = String(params["text"] ?? "");
9182
+ let mlClient2 = getMLClient();
9183
+ if (!mlClient2) {
9184
+ try {
9185
+ const cfg = getConfig();
9186
+ if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
9187
+ mlClient2 = initMLClient(cfg.ml.sidecarUrl);
9188
+ }
9189
+ } catch {
9190
+ }
9191
+ }
9192
+ if (!mlClient2) {
9193
+ return { error: "ML sidecar not configured for intent classification" };
9194
+ }
9195
+ const intent = await mlClient2.classifyIntent(text);
9196
+ if (!intent) {
9197
+ return { error: "Intent classification failed" };
9198
+ }
9199
+ return {
9200
+ intent: intent.intent,
9201
+ confidence: intent.confidence,
9202
+ secondaryIntent: intent.secondary_intent,
9203
+ detectedTokens: intent.detected_tokens,
9204
+ detectedAddresses: intent.detected_addresses,
9205
+ model: intent.model
9206
+ };
9207
+ }
9208
+ case "run_backtest": {
9209
+ const { BacktestEngine: BacktestEngine2 } = await Promise.resolve().then(() => (init_engine2(), engine_exports));
9210
+ const engine = new BacktestEngine2({
9211
+ strategy: String(params["strategy"] ?? "momentum"),
9212
+ pair: String(params["pair"] ?? "BTCUSDT"),
9213
+ from: String(params["from"] ?? ""),
9214
+ to: String(params["to"] ?? ""),
9215
+ initialCapital: 1e4,
9216
+ timeframe: String(params["timeframe"] ?? "4h"),
9217
+ slippageBps: 10,
9218
+ commissionPct: 0.1
9219
+ });
9220
+ const result = await engine.run();
9221
+ return {
9222
+ strategy: result.config.strategy,
9223
+ pair: result.config.pair,
9224
+ period: `${result.config.from} \u2192 ${result.config.to}`,
9225
+ metrics: result.metrics,
9226
+ tradeCount: result.trades.length,
9227
+ lastTrades: result.trades.slice(-5).map((t) => ({
9228
+ entry: t.entryPrice,
9229
+ exit: t.exitPrice,
9230
+ pnl: t.pnl,
9231
+ pnlPct: t.pnlPct,
9232
+ side: t.side
9233
+ }))
9234
+ };
9235
+ }
8223
9236
  default:
8224
9237
  return { error: `Unknown tool: ${name}` };
8225
9238
  }
@@ -8248,6 +9261,7 @@ var init_tool_handler = __esm({
8248
9261
  init_client();
8249
9262
  init_feature_engineer();
8250
9263
  init_store_factory();
9264
+ init_sanitize();
8251
9265
  }
8252
9266
  });
8253
9267
 
@@ -8651,6 +9665,59 @@ var init_tools = __esm({
8651
9665
  required: ["agentName"]
8652
9666
  }
8653
9667
  },
9668
+ {
9669
+ name: "get_ml_model_health",
9670
+ description: "Get health status of all ML models in the sidecar including loaded status, version, last training date, accuracy, uptime, and total predictions served.",
9671
+ input_schema: {
9672
+ type: "object",
9673
+ properties: {},
9674
+ required: []
9675
+ }
9676
+ },
9677
+ {
9678
+ name: "classify_user_intent",
9679
+ description: "Classify a user message into an intent category (e.g. price_check, token_analysis, prediction, news, portfolio, agent_management) using ML NLP. Returns intent, confidence, detected tokens, and detected addresses.",
9680
+ input_schema: {
9681
+ type: "object",
9682
+ properties: {
9683
+ text: {
9684
+ type: "string",
9685
+ description: "The user message text to classify."
9686
+ }
9687
+ },
9688
+ required: ["text"]
9689
+ }
9690
+ },
9691
+ {
9692
+ name: "run_backtest",
9693
+ description: "Run a historical backtest for a trading strategy. Simulates strategy execution on historical kline data and returns trades, metrics (return, win rate, Sharpe, drawdown), and equity curve.",
9694
+ input_schema: {
9695
+ type: "object",
9696
+ properties: {
9697
+ strategy: {
9698
+ type: "string",
9699
+ description: 'Strategy name: "momentum", "trend-following", or "ml-adaptive".'
9700
+ },
9701
+ pair: {
9702
+ type: "string",
9703
+ description: 'Trading pair (e.g. "BTCUSDT").'
9704
+ },
9705
+ from: {
9706
+ type: "string",
9707
+ description: "Start date (YYYY-MM-DD)."
9708
+ },
9709
+ to: {
9710
+ type: "string",
9711
+ description: "End date (YYYY-MM-DD)."
9712
+ },
9713
+ timeframe: {
9714
+ type: "string",
9715
+ description: 'Candle timeframe: "1h", "4h", "1d". Defaults to "4h".'
9716
+ }
9717
+ },
9718
+ required: ["strategy", "pair", "from", "to"]
9719
+ }
9720
+ },
8654
9721
  {
8655
9722
  name: "create_agent",
8656
9723
  description: "Create an autonomous trading agent that monitors crypto pairs using a strategy (momentum or trend-following). Returns the created agent config.",
@@ -9288,13 +10355,16 @@ async function startDiscordBot() {
9288
10355
  client2.on("messageCreate", async (message) => {
9289
10356
  if (message.author.bot) return;
9290
10357
  if (!client2.user || !message.mentions.has(client2.user)) return;
9291
- const text = message.content.replace(/<@!?\d+>/g, "").trim();
10358
+ let text = message.content.replace(/<@!?\d+>/g, "").trim();
9292
10359
  if (!text) {
9293
10360
  await message.reply(
9294
10361
  "Mention me with a question! e.g. `@Vizzor what is BTC price?`\nOr use slash commands: `/scan` `/trends` `/track` `/ico` `/audit` `/help`"
9295
10362
  );
9296
10363
  return;
9297
10364
  }
10365
+ if (text.length > 2e3) {
10366
+ text = text.slice(0, 2e3);
10367
+ }
9298
10368
  await message.reply("\u{1F52E} Analyzing...");
9299
10369
  try {
9300
10370
  const response = await analyze(buildChatSystemPrompt(), text, VIZZOR_TOOLS);
@@ -9964,8 +11034,11 @@ async function startTelegramBot() {
9964
11034
  bot.use(rateLimitMiddleware);
9965
11035
  registerCommands(bot);
9966
11036
  bot.on("message:text", async (ctx) => {
9967
- const text = ctx.message.text;
11037
+ let text = ctx.message.text;
9968
11038
  if (text.startsWith("/")) return;
11039
+ if (text.length > 4e3) {
11040
+ text = text.slice(0, 4e3);
11041
+ }
9969
11042
  await ctx.reply("\u{1F52E} Analyzing...");
9970
11043
  try {
9971
11044
  const response = await analyze(buildChatSystemPrompt(), text, VIZZOR_TOOLS);
@@ -10095,13 +11168,13 @@ var init_bot3 = __esm({
10095
11168
  });
10096
11169
 
10097
11170
  // src/data/collector.ts
10098
- var log2, MAJOR_SYMBOLS, TIMEFRAMES, COLLECTION_INTERVAL_MS, DataCollector;
11171
+ var log8, MAJOR_SYMBOLS, TIMEFRAMES, COLLECTION_INTERVAL_MS, DataCollector;
10099
11172
  var init_collector = __esm({
10100
11173
  "src/data/collector.ts"() {
10101
11174
  "use strict";
10102
11175
  init_binance();
10103
11176
  init_logger();
10104
- log2 = createLogger("collector");
11177
+ log8 = createLogger("collector");
10105
11178
  MAJOR_SYMBOLS = [
10106
11179
  "BTC",
10107
11180
  "ETH",
@@ -10152,10 +11225,10 @@ var init_collector = __esm({
10152
11225
  }
10153
11226
  start() {
10154
11227
  if (this.status.running) {
10155
- log2.warn("Collector already running");
11228
+ log8.warn("Collector already running");
10156
11229
  return;
10157
11230
  }
10158
- log2.info(
11231
+ log8.info(
10159
11232
  `Starting data collector: ${this.symbols.length} symbols, ${TIMEFRAMES.length} timeframes, every ${this.intervalMs / 1e3}s`
10160
11233
  );
10161
11234
  this.status.running = true;
@@ -10168,10 +11241,10 @@ var init_collector = __esm({
10168
11241
  this.timer = null;
10169
11242
  }
10170
11243
  this.status.running = false;
10171
- log2.info("Data collector stopped");
11244
+ log8.info("Data collector stopped");
10172
11245
  }
10173
11246
  async collectAll() {
10174
- log2.info("Collection cycle starting");
11247
+ log8.info("Collection cycle starting");
10175
11248
  const start = Date.now();
10176
11249
  for (const timeframe of TIMEFRAMES) {
10177
11250
  for (const symbol of this.symbols) {
@@ -10179,7 +11252,7 @@ var init_collector = __esm({
10179
11252
  await this.collectSymbol(symbol, timeframe);
10180
11253
  } catch (err) {
10181
11254
  this.status.errors++;
10182
- log2.error(
11255
+ log8.error(
10183
11256
  `Failed to collect ${symbol}/${timeframe}: ${err instanceof Error ? err.message : String(err)}`
10184
11257
  );
10185
11258
  }
@@ -10187,7 +11260,7 @@ var init_collector = __esm({
10187
11260
  }
10188
11261
  this.status.lastRun = Date.now();
10189
11262
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
10190
- log2.info(
11263
+ log8.info(
10191
11264
  `Collection cycle complete in ${elapsed}s (total records: ${this.status.totalRecords})`
10192
11265
  );
10193
11266
  }
@@ -10296,42 +11369,171 @@ async function registerMarketRoutes(server) {
10296
11369
  return handleTool("get_trending", {});
10297
11370
  }
10298
11371
  });
10299
- server.get("/fear-greed", {
11372
+ server.get("/fear-greed", {
11373
+ schema: {
11374
+ tags: ["Market"],
11375
+ summary: "Get Crypto Fear & Greed Index with history"
11376
+ },
11377
+ handler: async () => {
11378
+ return handleTool("get_fear_greed", {});
11379
+ }
11380
+ });
11381
+ server.get("/news", {
11382
+ schema: {
11383
+ tags: ["Market"],
11384
+ summary: "Get latest crypto news with sentiment",
11385
+ querystring: {
11386
+ type: "object",
11387
+ properties: { symbol: { type: "string" } }
11388
+ }
11389
+ },
11390
+ handler: async (request) => {
11391
+ try {
11392
+ const { symbol } = request.query;
11393
+ return await handleTool("get_crypto_news", { symbol });
11394
+ } catch {
11395
+ return { news: [] };
11396
+ }
11397
+ }
11398
+ });
11399
+ server.get("/prediction", {
11400
+ schema: {
11401
+ tags: ["Market"],
11402
+ summary: "Get AI prediction for a symbol",
11403
+ querystring: {
11404
+ type: "object",
11405
+ properties: { symbol: { type: "string" } },
11406
+ required: ["symbol"]
11407
+ }
11408
+ },
11409
+ handler: async (request) => {
11410
+ const { symbol } = request.query;
11411
+ return handleTool("get_prediction", { symbol });
11412
+ }
11413
+ });
11414
+ server.get("/dex/search", {
11415
+ schema: {
11416
+ tags: ["Market"],
11417
+ summary: "Search tokens on decentralized exchanges",
11418
+ querystring: {
11419
+ type: "object",
11420
+ properties: { q: { type: "string" } },
11421
+ required: ["q"]
11422
+ }
11423
+ },
11424
+ handler: async (request) => {
11425
+ const { q } = request.query;
11426
+ return handleTool("search_token_dex", { query: q });
11427
+ }
11428
+ });
11429
+ server.get("/ml-health", {
11430
+ schema: {
11431
+ tags: ["Market"],
11432
+ summary: "Get ML sidecar health and model status"
11433
+ },
11434
+ handler: async () => {
11435
+ let mlClient2 = getMLClient();
11436
+ if (!mlClient2) {
11437
+ const cfg = loadConfig();
11438
+ if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
11439
+ mlClient2 = initMLClient(cfg.ml.sidecarUrl);
11440
+ }
11441
+ }
11442
+ if (!mlClient2) {
11443
+ return { available: false, models: [], uptime: 0, predictionsServed: 0 };
11444
+ }
11445
+ const health = await mlClient2.getModelHealth();
11446
+ if (!health) {
11447
+ return { available: false, models: [], uptime: 0, predictionsServed: 0 };
11448
+ }
11449
+ return { available: true, ...health };
11450
+ }
11451
+ });
11452
+ server.post("/ml/sentiment", {
10300
11453
  schema: {
10301
11454
  tags: ["Market"],
10302
- summary: "Get Crypto Fear & Greed Index with history"
11455
+ summary: "Analyze text sentiment via ML",
11456
+ body: {
11457
+ type: "object",
11458
+ properties: { text: { type: "string" } },
11459
+ required: ["text"]
11460
+ }
10303
11461
  },
10304
- handler: async () => {
10305
- return handleTool("get_fear_greed", {});
11462
+ handler: async (request) => {
11463
+ const { text } = request.body;
11464
+ let mlClient2 = getMLClient();
11465
+ if (!mlClient2) {
11466
+ const cfg = loadConfig();
11467
+ if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
11468
+ mlClient2 = initMLClient(cfg.ml.sidecarUrl);
11469
+ }
11470
+ }
11471
+ if (!mlClient2) return { error: "ML sidecar unavailable" };
11472
+ const result = await mlClient2.analyzeSentiment(text);
11473
+ return result ?? { error: "Sentiment analysis failed" };
10306
11474
  }
10307
11475
  });
10308
- server.get("/news", {
11476
+ server.post("/ml/regime", {
10309
11477
  schema: {
10310
11478
  tags: ["Market"],
10311
- summary: "Get latest crypto news with sentiment",
10312
- querystring: {
11479
+ summary: "Detect market regime via ML",
11480
+ body: {
10313
11481
  type: "object",
10314
- properties: { symbol: { type: "string" } }
11482
+ properties: {
11483
+ returns_1d: { type: "number" },
11484
+ returns_7d: { type: "number" },
11485
+ volatility_14d: { type: "number" },
11486
+ volume_ratio: { type: "number" },
11487
+ rsi: { type: "number" },
11488
+ bb_width: { type: "number" },
11489
+ fear_greed: { type: "number" },
11490
+ funding_rate: { type: "number" },
11491
+ price_vs_sma200: { type: "number" }
11492
+ }
10315
11493
  }
10316
11494
  },
10317
11495
  handler: async (request) => {
10318
- const { symbol } = request.query;
10319
- return handleTool("get_crypto_news", { symbol });
11496
+ const features = request.body;
11497
+ let mlClient2 = getMLClient();
11498
+ if (!mlClient2) {
11499
+ const cfg = loadConfig();
11500
+ if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
11501
+ mlClient2 = initMLClient(cfg.ml.sidecarUrl);
11502
+ }
11503
+ }
11504
+ if (!mlClient2) return { error: "ML sidecar unavailable" };
11505
+ const result = await mlClient2.detectRegime(features);
11506
+ return result ?? { error: "Regime detection failed" };
10320
11507
  }
10321
11508
  });
10322
- server.get("/dex/search", {
11509
+ server.post("/ml/trend", {
10323
11510
  schema: {
10324
11511
  tags: ["Market"],
10325
- summary: "Search tokens on decentralized exchanges",
10326
- querystring: {
11512
+ summary: "Score trend via ML",
11513
+ body: {
10327
11514
  type: "object",
10328
- properties: { q: { type: "string" } },
10329
- required: ["q"]
11515
+ properties: {
11516
+ price_change_24h: { type: "number" },
11517
+ price_change_7d: { type: "number" },
11518
+ volume_24h: { type: "number" },
11519
+ market_cap: { type: "number" },
11520
+ volume_to_mcap_ratio: { type: "number" },
11521
+ rank: { type: "number" }
11522
+ }
10330
11523
  }
10331
11524
  },
10332
11525
  handler: async (request) => {
10333
- const { q } = request.query;
10334
- return handleTool("search_token_dex", { query: q });
11526
+ const features = request.body;
11527
+ let mlClient2 = getMLClient();
11528
+ if (!mlClient2) {
11529
+ const cfg = loadConfig();
11530
+ if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
11531
+ mlClient2 = initMLClient(cfg.ml.sidecarUrl);
11532
+ }
11533
+ }
11534
+ if (!mlClient2) return { error: "ML sidecar unavailable" };
11535
+ const result = await mlClient2.scoreTrend(features);
11536
+ return result ?? { error: "Trend scoring failed" };
10335
11537
  }
10336
11538
  });
10337
11539
  server.get("/derivatives/:symbol", {
@@ -10350,6 +11552,8 @@ var init_market3 = __esm({
10350
11552
  "src/api/routes/v1/market.ts"() {
10351
11553
  "use strict";
10352
11554
  init_tool_handler();
11555
+ init_client();
11556
+ init_loader();
10353
11557
  }
10354
11558
  });
10355
11559
 
@@ -10482,6 +11686,134 @@ var init_security = __esm({
10482
11686
  }
10483
11687
  });
10484
11688
 
11689
+ // src/api/routes/v1/backtest.ts
11690
+ async function backtestRoutes(fastify) {
11691
+ fastify.post("/v1/backtest", async (request, reply) => {
11692
+ const body = request.body;
11693
+ const config2 = {
11694
+ strategy: String(body["strategy"] ?? "momentum"),
11695
+ pair: String(body["pair"] ?? "BTCUSDT"),
11696
+ from: String(body["from"] ?? ""),
11697
+ to: String(body["to"] ?? ""),
11698
+ initialCapital: Number(body["initialCapital"] ?? 1e4),
11699
+ timeframe: String(body["timeframe"] ?? "4h"),
11700
+ slippageBps: Number(body["slippageBps"] ?? 10),
11701
+ commissionPct: Number(body["commissionPct"] ?? 0.1)
11702
+ };
11703
+ if (!config2.from || !config2.to) {
11704
+ return reply.status(400).send({ error: "from and to dates are required" });
11705
+ }
11706
+ const engine = new BacktestEngine(config2);
11707
+ const result = await engine.run();
11708
+ return result;
11709
+ });
11710
+ }
11711
+ var init_backtest = __esm({
11712
+ "src/api/routes/v1/backtest.ts"() {
11713
+ "use strict";
11714
+ init_engine2();
11715
+ }
11716
+ });
11717
+
11718
+ // src/api/routes/v1/agents.ts
11719
+ async function agentRoutes(fastify) {
11720
+ fastify.get("/v1/agents", async () => {
11721
+ const agents = listAgents();
11722
+ return {
11723
+ agents: agents.map((a) => {
11724
+ const status = getAgentStatus(a.id);
11725
+ return {
11726
+ name: a.name,
11727
+ strategy: a.strategy,
11728
+ pairs: a.pairs,
11729
+ interval: a.interval,
11730
+ status: status?.status ?? "idle",
11731
+ cycleCount: status?.cycleCount ?? 0
11732
+ };
11733
+ })
11734
+ };
11735
+ });
11736
+ fastify.post("/v1/agents", async (request) => {
11737
+ const body = request.body;
11738
+ const name = String(body["name"] ?? "");
11739
+ const strategy = String(body["strategy"] ?? "momentum");
11740
+ const pairs = body["pairs"] ?? ["BTC", "ETH"];
11741
+ const interval = Number(body["interval"] ?? 60);
11742
+ const agent = createAgent(name, strategy, pairs, interval);
11743
+ return { id: agent.id, name: agent.name, strategy: agent.strategy, pairs: agent.pairs };
11744
+ });
11745
+ fastify.get("/v1/agents/:name", async (request) => {
11746
+ const { name } = request.params;
11747
+ const agent = getAgentByName(name);
11748
+ if (!agent) return { error: "Agent not found" };
11749
+ const status = getAgentStatus(agent.id);
11750
+ return {
11751
+ name: agent.name,
11752
+ strategy: agent.strategy,
11753
+ pairs: agent.pairs,
11754
+ status: status?.status ?? "idle",
11755
+ cycleCount: status?.cycleCount ?? 0
11756
+ };
11757
+ });
11758
+ fastify.post("/v1/agents/:name/start", async (request) => {
11759
+ const { name } = request.params;
11760
+ const agent = getAgentByName(name);
11761
+ if (!agent) return { error: "Agent not found" };
11762
+ startAgent(agent.id);
11763
+ return { message: `Agent "${name}" started` };
11764
+ });
11765
+ fastify.post("/v1/agents/:name/stop", async (request) => {
11766
+ const { name } = request.params;
11767
+ const agent = getAgentByName(name);
11768
+ if (!agent) return { error: "Agent not found" };
11769
+ stopAgent(agent.id);
11770
+ return { message: `Agent "${name}" stopped` };
11771
+ });
11772
+ fastify.delete("/v1/agents/:name", async (request) => {
11773
+ const { name } = request.params;
11774
+ const agent = getAgentByName(name);
11775
+ if (!agent) return { error: "Agent not found" };
11776
+ deleteAgent(agent.id);
11777
+ return { message: `Agent "${name}" deleted` };
11778
+ });
11779
+ }
11780
+ var init_agents = __esm({
11781
+ "src/api/routes/v1/agents.ts"() {
11782
+ "use strict";
11783
+ init_agent();
11784
+ }
11785
+ });
11786
+
11787
+ // src/api/routes/v1/portfolio.ts
11788
+ async function portfolioRoutes(fastify) {
11789
+ fastify.get("/v1/portfolio/:agentId", async (request) => {
11790
+ const { agentId } = request.params;
11791
+ return {
11792
+ agentId,
11793
+ totalValue: 1e4,
11794
+ cash: 1e4,
11795
+ positions: [],
11796
+ totalReturn: 0,
11797
+ totalReturnPct: 0,
11798
+ winRate: 0,
11799
+ sharpeRatio: 0,
11800
+ maxDrawdown: 0
11801
+ };
11802
+ });
11803
+ fastify.get("/v1/portfolio/:agentId/trades", async (request) => {
11804
+ const { agentId } = request.params;
11805
+ return {
11806
+ agentId,
11807
+ trades: []
11808
+ };
11809
+ });
11810
+ }
11811
+ var init_portfolio = __esm({
11812
+ "src/api/routes/v1/portfolio.ts"() {
11813
+ "use strict";
11814
+ }
11815
+ });
11816
+
10485
11817
  // src/api/auth/keys.ts
10486
11818
  import { randomBytes, scryptSync } from "crypto";
10487
11819
  function hashApiKey(key) {
@@ -10543,19 +11875,26 @@ var init_keys2 = __esm({
10543
11875
  });
10544
11876
 
10545
11877
  // src/api/auth/middleware.ts
11878
+ import { timingSafeEqual } from "crypto";
10546
11879
  async function authMiddleware(request, reply) {
10547
- if (PUBLIC_PATHS.some((p) => request.url === p) || request.url.startsWith("/docs/")) {
11880
+ if (PUBLIC_PATHS.some((p) => request.url === p)) {
11881
+ return;
11882
+ }
11883
+ if (request.url === "/docs" || request.url.startsWith("/docs/")) {
11884
+ if (process.env["NODE_ENV"] === "production") {
11885
+ return reply.status(404).send({ error: "Not found" });
11886
+ }
10548
11887
  return;
10549
11888
  }
10550
11889
  const apiKey = request.headers["x-api-key"];
10551
- if (!apiKey) {
11890
+ if (!apiKey || apiKey.length > 256) {
10552
11891
  return reply.status(401).send({
10553
11892
  error: "Unauthorized",
10554
- message: "Missing X-API-Key header"
11893
+ message: "Missing or invalid X-API-Key header"
10555
11894
  });
10556
11895
  }
10557
11896
  const keyHash = hashApiKey(apiKey);
10558
- const valid = await validateKey2(keyHash);
11897
+ const valid = validateKey2(keyHash);
10559
11898
  if (!valid) {
10560
11899
  return reply.status(403).send({
10561
11900
  error: "Forbidden",
@@ -10563,20 +11902,43 @@ async function authMiddleware(request, reply) {
10563
11902
  });
10564
11903
  }
10565
11904
  }
10566
- async function validateKey2(keyHash) {
10567
- const store = getStoreInstance();
10568
- if (!store) return false;
10569
- const cached = await store.getCached(`apikey:${keyHash}`);
10570
- if (cached !== null) return cached.valid;
10571
- return true;
11905
+ function validateKey2(keyHash) {
11906
+ try {
11907
+ const db2 = getDb();
11908
+ db2.exec(`
11909
+ CREATE TABLE IF NOT EXISTS api_keys (
11910
+ id TEXT PRIMARY KEY,
11911
+ label TEXT NOT NULL,
11912
+ key_hash TEXT NOT NULL UNIQUE,
11913
+ key_prefix TEXT NOT NULL,
11914
+ rate_limit INTEGER NOT NULL DEFAULT 100,
11915
+ created_at INTEGER NOT NULL,
11916
+ revoked_at INTEGER
11917
+ )
11918
+ `);
11919
+ const row = db2.prepare("SELECT key_hash FROM api_keys WHERE revoked_at IS NULL").all();
11920
+ if (row.length === 0) {
11921
+ return false;
11922
+ }
11923
+ const inputBuf = Buffer.from(keyHash, "hex");
11924
+ for (const r of row) {
11925
+ const storedBuf = Buffer.from(r.key_hash, "hex");
11926
+ if (inputBuf.length === storedBuf.length && timingSafeEqual(inputBuf, storedBuf)) {
11927
+ return true;
11928
+ }
11929
+ }
11930
+ return false;
11931
+ } catch {
11932
+ return false;
11933
+ }
10572
11934
  }
10573
11935
  var PUBLIC_PATHS;
10574
11936
  var init_middleware = __esm({
10575
11937
  "src/api/auth/middleware.ts"() {
10576
11938
  "use strict";
10577
11939
  init_keys2();
10578
- init_store_factory();
10579
- PUBLIC_PATHS = ["/health", "/docs", "/docs/"];
11940
+ init_cache();
11941
+ PUBLIC_PATHS = ["/health"];
10580
11942
  }
10581
11943
  });
10582
11944
 
@@ -10613,7 +11975,11 @@ import swagger from "@fastify/swagger";
10613
11975
  import swaggerUi from "@fastify/swagger-ui";
10614
11976
  async function startApiServer(options) {
10615
11977
  const server = Fastify({ logger: false });
10616
- await server.register(cors, { origin: true });
11978
+ const isProd = process.env["NODE_ENV"] === "production";
11979
+ const origin = options.corsOrigin ?? "http://localhost:3000";
11980
+ await server.register(cors, {
11981
+ origin: isProd ? origin : true
11982
+ });
10617
11983
  await server.register(rateLimit, {
10618
11984
  max: 100,
10619
11985
  timeWindow: "1 minute"
@@ -10623,7 +11989,7 @@ async function startApiServer(options) {
10623
11989
  info: {
10624
11990
  title: "Vizzor API",
10625
11991
  description: "AI-powered crypto intelligence REST API",
10626
- version: "0.7.0"
11992
+ version: "0.11.0"
10627
11993
  },
10628
11994
  servers: [{ url: `http://${options.host}:${options.port}` }],
10629
11995
  components: {
@@ -10637,27 +12003,41 @@ async function startApiServer(options) {
10637
12003
  }
10638
12004
  }
10639
12005
  });
10640
- await server.register(swaggerUi, {
10641
- routePrefix: "/docs"
10642
- });
10643
- if (options.enableAuth) {
12006
+ if (!isProd) {
12007
+ await server.register(swaggerUi, {
12008
+ routePrefix: "/docs"
12009
+ });
12010
+ }
12011
+ if (options.enableAuth !== false) {
10644
12012
  server.addHook("onRequest", authMiddleware);
12013
+ } else if (isProd) {
12014
+ log9.warn("API authentication is DISABLED in production \u2014 this is insecure");
10645
12015
  }
10646
12016
  server.setErrorHandler(errorHandler);
10647
- server.get("/health", async () => ({
10648
- status: "ok",
10649
- version: "0.7.0",
10650
- uptime: process.uptime(),
10651
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
10652
- }));
12017
+ server.get("/health", async () => {
12018
+ if (isProd) {
12019
+ return { status: "ok" };
12020
+ }
12021
+ return {
12022
+ status: "ok",
12023
+ version: "0.11.0",
12024
+ uptime: process.uptime(),
12025
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
12026
+ };
12027
+ });
10653
12028
  await server.register(registerMarketRoutes, { prefix: "/v1/market" });
10654
12029
  await server.register(registerAnalysisRoutes, { prefix: "/v1/analysis" });
10655
12030
  await server.register(registerSecurityRoutes, { prefix: "/v1/security" });
12031
+ await server.register(backtestRoutes);
12032
+ await server.register(agentRoutes);
12033
+ await server.register(portfolioRoutes);
10656
12034
  await server.listen({ port: options.port, host: options.host });
10657
- log3.info(`Vizzor API listening on ${options.host}:${options.port}`);
10658
- log3.info(`OpenAPI docs at http://${options.host}:${options.port}/docs`);
12035
+ log9.info(`Vizzor API listening on ${options.host}:${options.port}`);
12036
+ if (!isProd) {
12037
+ log9.info(`OpenAPI docs at http://${options.host}:${options.port}/docs`);
12038
+ }
10659
12039
  }
10660
- var log3;
12040
+ var log9;
10661
12041
  var init_server = __esm({
10662
12042
  "src/api/server.ts"() {
10663
12043
  "use strict";
@@ -10665,9 +12045,12 @@ var init_server = __esm({
10665
12045
  init_market3();
10666
12046
  init_analysis();
10667
12047
  init_security();
12048
+ init_backtest();
12049
+ init_agents();
12050
+ init_portfolio();
10668
12051
  init_middleware();
10669
12052
  init_error_handler();
10670
- log3 = createLogger("api");
12053
+ log9 = createLogger("api");
10671
12054
  }
10672
12055
  });
10673
12056
 
@@ -10749,6 +12132,480 @@ var init_api = __esm({
10749
12132
  }
10750
12133
  });
10751
12134
 
12135
+ // src/cli/commands/backtest.ts
12136
+ var backtest_exports = {};
12137
+ __export(backtest_exports, {
12138
+ registerBacktestCommand: () => registerBacktestCommand
12139
+ });
12140
+ function registerBacktestCommand(program2) {
12141
+ program2.command("backtest").description("Run a historical backtest for a trading strategy").requiredOption(
12142
+ "-s, --strategy <strategy>",
12143
+ "Strategy name (momentum, trend-following, ml-adaptive)"
12144
+ ).requiredOption("--pair <pair>", "Trading pair (e.g. BTCUSDT)").requiredOption("--from <date>", "Start date (YYYY-MM-DD)").requiredOption("--to <date>", "End date (YYYY-MM-DD)").option("-c, --capital <amount>", "Initial capital in USD", "10000").option("-t, --timeframe <tf>", "Candle timeframe", "4h").option("--slippage <bps>", "Slippage in basis points", "10").option("--commission <pct>", "Commission percentage", "0.1").action(async (opts) => {
12145
+ try {
12146
+ const engine = new BacktestEngine({
12147
+ strategy: opts.strategy,
12148
+ pair: opts.pair,
12149
+ from: opts.from,
12150
+ to: opts.to,
12151
+ initialCapital: Number(opts.capital),
12152
+ timeframe: opts.timeframe,
12153
+ slippageBps: Number(opts.slippage),
12154
+ commissionPct: Number(opts.commission)
12155
+ });
12156
+ console.log(
12157
+ `Running backtest: ${opts.strategy} on ${opts.pair} (${opts.from} \u2192 ${opts.to})...
12158
+ `
12159
+ );
12160
+ const result = await engine.run();
12161
+ console.log("=== Backtest Results ===");
12162
+ console.log(`Strategy: ${result.config.strategy}`);
12163
+ console.log(`Pair: ${result.config.pair}`);
12164
+ console.log(`Period: ${result.config.from} \u2192 ${result.config.to}`);
12165
+ console.log(`Initial Capital: $${result.config.initialCapital.toLocaleString()}`);
12166
+ console.log("");
12167
+ console.log(
12168
+ `Total Return: $${result.metrics.totalReturn.toFixed(2)} (${result.metrics.totalReturnPct.toFixed(2)}%)`
12169
+ );
12170
+ console.log(`Win Rate: ${(result.metrics.winRate * 100).toFixed(1)}%`);
12171
+ console.log(`Total Trades: ${result.metrics.totalTrades}`);
12172
+ console.log(
12173
+ `Profit Factor: ${result.metrics.profitFactor === Infinity ? "\u221E" : result.metrics.profitFactor.toFixed(2)}`
12174
+ );
12175
+ console.log(`Sharpe Ratio: ${result.metrics.sharpeRatio.toFixed(2)}`);
12176
+ console.log(`Max Drawdown: ${result.metrics.maxDrawdown.toFixed(2)}%`);
12177
+ if (result.trades.length > 0) {
12178
+ console.log("\nLast 5 Trades:");
12179
+ for (const t of result.trades.slice(-5)) {
12180
+ const dir = t.pnl >= 0 ? "+" : "";
12181
+ console.log(
12182
+ ` ${new Date(t.entryTime).toISOString().split("T")[0]} \u2192 ${new Date(t.exitTime).toISOString().split("T")[0]} | ${t.side} | Entry: $${t.entryPrice.toFixed(2)} \u2192 Exit: $${t.exitPrice.toFixed(2)} | PnL: ${dir}$${t.pnl.toFixed(2)} (${dir}${t.pnlPct.toFixed(2)}%)`
12183
+ );
12184
+ }
12185
+ }
12186
+ } catch (err) {
12187
+ log10.error(`Backtest failed: ${err instanceof Error ? err.message : String(err)}`);
12188
+ process.exitCode = 1;
12189
+ }
12190
+ });
12191
+ }
12192
+ var log10;
12193
+ var init_backtest2 = __esm({
12194
+ "src/cli/commands/backtest.ts"() {
12195
+ "use strict";
12196
+ init_engine2();
12197
+ init_logger();
12198
+ log10 = createLogger("cli-backtest");
12199
+ }
12200
+ });
12201
+
12202
+ // src/core/agent/wallet-manager.ts
12203
+ import { randomBytes as randomBytes2, scryptSync as scryptSync2, createCipheriv, createDecipheriv } from "crypto";
12204
+ import {
12205
+ existsSync as existsSync3,
12206
+ mkdirSync as mkdirSync2,
12207
+ readFileSync as readFileSync3,
12208
+ writeFileSync as writeFileSync3,
12209
+ unlinkSync,
12210
+ readdirSync
12211
+ } from "fs";
12212
+ import { join as join3 } from "path";
12213
+ import { homedir as homedir2 } from "os";
12214
+ function ensureDir() {
12215
+ if (!existsSync3(WALLETS_DIR)) {
12216
+ mkdirSync2(WALLETS_DIR, { recursive: true });
12217
+ }
12218
+ }
12219
+ function deriveKey(password, salt) {
12220
+ return scryptSync2(password, salt, KEY_LENGTH, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
12221
+ }
12222
+ function encrypt(plaintext, password) {
12223
+ const salt = randomBytes2(32);
12224
+ const key = deriveKey(password, salt);
12225
+ const iv = randomBytes2(IV_LENGTH);
12226
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
12227
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
12228
+ const tag = cipher.getAuthTag();
12229
+ return {
12230
+ salt: salt.toString("hex"),
12231
+ iv: iv.toString("hex"),
12232
+ tag: tag.toString("hex"),
12233
+ ciphertext: encrypted.toString("hex")
12234
+ };
12235
+ }
12236
+ function createWallet(name, password) {
12237
+ ensureDir();
12238
+ const filePath = join3(WALLETS_DIR, `${name}.json`);
12239
+ if (existsSync3(filePath)) {
12240
+ throw new Error(`Wallet "${name}" already exists`);
12241
+ }
12242
+ const privateKey = `0x${randomBytes2(32).toString("hex")}`;
12243
+ const { salt, iv, tag, ciphertext } = encrypt(privateKey, password);
12244
+ const wallet = {
12245
+ name,
12246
+ address: "(derived on load)",
12247
+ salt,
12248
+ iv,
12249
+ tag,
12250
+ ciphertext,
12251
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
12252
+ };
12253
+ writeFileSync3(filePath, JSON.stringify(wallet, null, 2));
12254
+ log11.info(`Wallet "${name}" created at ${filePath}`);
12255
+ return { name, address: wallet.address };
12256
+ }
12257
+ function importWallet(name, privateKey, password) {
12258
+ ensureDir();
12259
+ const filePath = join3(WALLETS_DIR, `${name}.json`);
12260
+ if (existsSync3(filePath)) {
12261
+ throw new Error(`Wallet "${name}" already exists`);
12262
+ }
12263
+ const pk = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
12264
+ const { salt, iv, tag, ciphertext } = encrypt(pk, password);
12265
+ const wallet = {
12266
+ name,
12267
+ address: "(derived on load)",
12268
+ salt,
12269
+ iv,
12270
+ tag,
12271
+ ciphertext,
12272
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
12273
+ };
12274
+ writeFileSync3(filePath, JSON.stringify(wallet, null, 2));
12275
+ log11.info(`Wallet "${name}" imported`);
12276
+ return { name, address: wallet.address };
12277
+ }
12278
+ function listWallets() {
12279
+ ensureDir();
12280
+ const files = readdirSync(WALLETS_DIR).filter((f) => f.endsWith(".json"));
12281
+ return files.map((f) => {
12282
+ const wallet = JSON.parse(readFileSync3(join3(WALLETS_DIR, f), "utf8"));
12283
+ return { name: wallet.name, address: wallet.address, createdAt: wallet.createdAt };
12284
+ });
12285
+ }
12286
+ function deleteWallet(name) {
12287
+ const filePath = join3(WALLETS_DIR, `${name}.json`);
12288
+ if (!existsSync3(filePath)) {
12289
+ throw new Error(`Wallet "${name}" not found`);
12290
+ }
12291
+ unlinkSync(filePath);
12292
+ log11.info(`Wallet "${name}" deleted`);
12293
+ }
12294
+ var log11, WALLETS_DIR, SCRYPT_N, SCRYPT_R, SCRYPT_P, KEY_LENGTH, IV_LENGTH;
12295
+ var init_wallet_manager = __esm({
12296
+ "src/core/agent/wallet-manager.ts"() {
12297
+ "use strict";
12298
+ init_logger();
12299
+ log11 = createLogger("wallet-manager");
12300
+ WALLETS_DIR = join3(homedir2(), ".vizzor", "wallets");
12301
+ SCRYPT_N = 2 ** 14;
12302
+ SCRYPT_R = 8;
12303
+ SCRYPT_P = 1;
12304
+ KEY_LENGTH = 32;
12305
+ IV_LENGTH = 12;
12306
+ }
12307
+ });
12308
+
12309
+ // src/cli/commands/wallet.ts
12310
+ var wallet_exports = {};
12311
+ __export(wallet_exports, {
12312
+ registerWalletCommand: () => registerWalletCommand
12313
+ });
12314
+ function registerWalletCommand(program2) {
12315
+ const wallet = program2.command("wallet").description("Manage encrypted trading wallets");
12316
+ wallet.command("create <name>").description("Create a new wallet with a random private key").requiredOption("-p, --password <password>", "Encryption password").action((name, opts) => {
12317
+ try {
12318
+ const result = createWallet(name, opts.password);
12319
+ console.log(`Wallet "${result.name}" created successfully.`);
12320
+ console.log(`Address: ${result.address}`);
12321
+ console.log("Stored in ~/.vizzor/wallets/");
12322
+ } catch (err) {
12323
+ log12.error(`Failed to create wallet: ${err instanceof Error ? err.message : String(err)}`);
12324
+ process.exitCode = 1;
12325
+ }
12326
+ });
12327
+ wallet.command("import <name>").description("Import a wallet from a private key").requiredOption("-k, --key <privateKey>", "Private key (hex)").requiredOption("-p, --password <password>", "Encryption password").action((name, opts) => {
12328
+ try {
12329
+ const result = importWallet(name, opts.key, opts.password);
12330
+ console.log(`Wallet "${result.name}" imported successfully.`);
12331
+ } catch (err) {
12332
+ log12.error(`Failed to import wallet: ${err instanceof Error ? err.message : String(err)}`);
12333
+ process.exitCode = 1;
12334
+ }
12335
+ });
12336
+ wallet.command("list").description("List all saved wallets").action(() => {
12337
+ const wallets = listWallets();
12338
+ if (wallets.length === 0) {
12339
+ console.log("No wallets found.");
12340
+ return;
12341
+ }
12342
+ console.log("Wallets:");
12343
+ for (const w of wallets) {
12344
+ console.log(` ${w.name} \u2014 ${w.address} (created: ${w.createdAt})`);
12345
+ }
12346
+ });
12347
+ wallet.command("delete <name>").description("Delete a wallet").action((name) => {
12348
+ try {
12349
+ deleteWallet(name);
12350
+ console.log(`Wallet "${name}" deleted.`);
12351
+ } catch (err) {
12352
+ log12.error(`Failed to delete wallet: ${err instanceof Error ? err.message : String(err)}`);
12353
+ process.exitCode = 1;
12354
+ }
12355
+ });
12356
+ }
12357
+ var log12;
12358
+ var init_wallet2 = __esm({
12359
+ "src/cli/commands/wallet.ts"() {
12360
+ "use strict";
12361
+ init_wallet_manager();
12362
+ init_logger();
12363
+ log12 = createLogger("cli-wallet");
12364
+ }
12365
+ });
12366
+
12367
+ // src/data/sources/binance-ws.ts
12368
+ import { EventEmitter } from "events";
12369
+ var log13, BASE_URL7, BinanceWebSocket;
12370
+ var init_binance_ws = __esm({
12371
+ "src/data/sources/binance-ws.ts"() {
12372
+ "use strict";
12373
+ init_logger();
12374
+ log13 = createLogger("binance-ws");
12375
+ BASE_URL7 = "wss://stream.binance.com:9443/ws";
12376
+ BinanceWebSocket = class extends EventEmitter {
12377
+ ws = null;
12378
+ streams = [];
12379
+ reconnectDelay = 1e3;
12380
+ maxReconnectDelay = 3e4;
12381
+ heartbeatTimer = null;
12382
+ reconnecting = false;
12383
+ closed = false;
12384
+ constructor(streams) {
12385
+ super();
12386
+ this.streams = streams;
12387
+ }
12388
+ connect() {
12389
+ if (this.closed) return;
12390
+ const url = this.streams.length === 1 ? `${BASE_URL7}/${this.streams[0]}` : `${BASE_URL7}/${this.streams.join("/")}`;
12391
+ log13.debug(`Connecting to ${url}`);
12392
+ this.ws = new WebSocket(url);
12393
+ this.ws.onopen = () => {
12394
+ log13.info(`WebSocket connected (${this.streams.length} streams)`);
12395
+ this.reconnectDelay = 1e3;
12396
+ this.reconnecting = false;
12397
+ this.startHeartbeat();
12398
+ this.emit("connected");
12399
+ };
12400
+ this.ws.onmessage = (event) => {
12401
+ try {
12402
+ const data = JSON.parse(String(event.data));
12403
+ this.handleMessage(data);
12404
+ } catch {
12405
+ }
12406
+ };
12407
+ this.ws.onclose = () => {
12408
+ this.stopHeartbeat();
12409
+ if (!this.closed) {
12410
+ this.scheduleReconnect();
12411
+ }
12412
+ };
12413
+ this.ws.onerror = () => {
12414
+ log13.debug("WebSocket error");
12415
+ };
12416
+ }
12417
+ close() {
12418
+ this.closed = true;
12419
+ this.stopHeartbeat();
12420
+ if (this.ws) {
12421
+ this.ws.close();
12422
+ this.ws = null;
12423
+ }
12424
+ }
12425
+ handleMessage(data) {
12426
+ const eventType = data["e"];
12427
+ if (eventType === "trade") {
12428
+ const trade = {
12429
+ symbol: String(data["s"]),
12430
+ price: Number(data["p"]),
12431
+ quantity: Number(data["q"]),
12432
+ time: Number(data["T"]),
12433
+ isBuyerMaker: Boolean(data["m"])
12434
+ };
12435
+ this.emit("trade", trade);
12436
+ } else if (eventType === "kline") {
12437
+ const k = data["k"];
12438
+ const kline = {
12439
+ symbol: String(data["s"]),
12440
+ interval: String(k["i"]),
12441
+ open: Number(k["o"]),
12442
+ high: Number(k["h"]),
12443
+ low: Number(k["l"]),
12444
+ close: Number(k["c"]),
12445
+ volume: Number(k["v"]),
12446
+ openTime: Number(k["t"]),
12447
+ closeTime: Number(k["T"]),
12448
+ isClosed: Boolean(k["x"])
12449
+ };
12450
+ this.emit("kline", kline);
12451
+ } else if (eventType === "24hrTicker") {
12452
+ const ticker = {
12453
+ symbol: String(data["s"]),
12454
+ price: Number(data["c"]),
12455
+ priceChange: Number(data["p"]),
12456
+ priceChangePct: Number(data["P"]),
12457
+ volume: Number(data["v"]),
12458
+ quoteVolume: Number(data["q"])
12459
+ };
12460
+ this.emit("ticker", ticker);
12461
+ }
12462
+ }
12463
+ scheduleReconnect() {
12464
+ if (this.reconnecting || this.closed) return;
12465
+ this.reconnecting = true;
12466
+ log13.info(`Reconnecting in ${this.reconnectDelay}ms...`);
12467
+ setTimeout(() => {
12468
+ this.connect();
12469
+ }, this.reconnectDelay);
12470
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
12471
+ }
12472
+ startHeartbeat() {
12473
+ this.heartbeatTimer = setInterval(() => {
12474
+ if (this.ws?.readyState === WebSocket.OPEN) {
12475
+ this.ws.send(JSON.stringify({ method: "PING" }));
12476
+ }
12477
+ }, 3e4);
12478
+ }
12479
+ stopHeartbeat() {
12480
+ if (this.heartbeatTimer) {
12481
+ clearInterval(this.heartbeatTimer);
12482
+ this.heartbeatTimer = null;
12483
+ }
12484
+ }
12485
+ };
12486
+ }
12487
+ });
12488
+
12489
+ // src/data/sources/ws-manager.ts
12490
+ var ws_manager_exports = {};
12491
+ __export(ws_manager_exports, {
12492
+ WSManager: () => WSManager,
12493
+ getWSManager: () => getWSManager,
12494
+ initWSManager: () => initWSManager
12495
+ });
12496
+ function getWSManager() {
12497
+ return instance2;
12498
+ }
12499
+ function initWSManager() {
12500
+ if (!instance2) {
12501
+ instance2 = new WSManager();
12502
+ }
12503
+ return instance2;
12504
+ }
12505
+ var log14, MAX_STREAMS_PER_CONN, MAX_CONNECTIONS, WSManager, instance2;
12506
+ var init_ws_manager = __esm({
12507
+ "src/data/sources/ws-manager.ts"() {
12508
+ "use strict";
12509
+ init_binance_ws();
12510
+ init_logger();
12511
+ log14 = createLogger("ws-manager");
12512
+ MAX_STREAMS_PER_CONN = 1024;
12513
+ MAX_CONNECTIONS = 5;
12514
+ WSManager = class {
12515
+ connections = [];
12516
+ priceCache = /* @__PURE__ */ new Map();
12517
+ subscriptions = /* @__PURE__ */ new Map();
12518
+ // symbol → stream types
12519
+ tradeCallbacks = /* @__PURE__ */ new Map();
12520
+ klineCallbacks = /* @__PURE__ */ new Map();
12521
+ started = false;
12522
+ subscribe(symbol, streams = ["trade", "ticker"]) {
12523
+ const existing = this.subscriptions.get(symbol) ?? /* @__PURE__ */ new Set();
12524
+ for (const s of streams) {
12525
+ existing.add(s);
12526
+ }
12527
+ this.subscriptions.set(symbol, existing);
12528
+ }
12529
+ unsubscribe(symbol) {
12530
+ this.subscriptions.delete(symbol);
12531
+ this.priceCache.delete(symbol.toUpperCase());
12532
+ }
12533
+ start() {
12534
+ if (this.started) return;
12535
+ this.started = true;
12536
+ const allStreams = [];
12537
+ for (const [symbol, types] of this.subscriptions) {
12538
+ const lower = symbol.toLowerCase();
12539
+ for (const type of types) {
12540
+ if (type === "trade") allStreams.push(`${lower}@trade`);
12541
+ else if (type === "kline_1m") allStreams.push(`${lower}@kline_1m`);
12542
+ else if (type === "ticker") allStreams.push(`${lower}@ticker`);
12543
+ }
12544
+ }
12545
+ if (allStreams.length === 0) {
12546
+ log14.info("No subscriptions \u2014 WebSocket manager idle");
12547
+ return;
12548
+ }
12549
+ for (let i = 0; i < allStreams.length; i += MAX_STREAMS_PER_CONN) {
12550
+ if (this.connections.length >= MAX_CONNECTIONS) break;
12551
+ const chunk = allStreams.slice(i, i + MAX_STREAMS_PER_CONN);
12552
+ const ws = new BinanceWebSocket(chunk);
12553
+ ws.on("trade", (trade) => {
12554
+ this.priceCache.set(trade.symbol, { price: trade.price, time: trade.time });
12555
+ const callbacks = this.tradeCallbacks.get(trade.symbol);
12556
+ if (callbacks) {
12557
+ for (const cb of callbacks) cb(trade);
12558
+ }
12559
+ });
12560
+ ws.on("ticker", (ticker) => {
12561
+ this.priceCache.set(ticker.symbol, { price: ticker.price, time: Date.now() });
12562
+ });
12563
+ ws.on("kline", (kline) => {
12564
+ const key = `${kline.symbol}:${kline.interval}`;
12565
+ const callbacks = this.klineCallbacks.get(key);
12566
+ if (callbacks) {
12567
+ for (const cb of callbacks) cb(kline);
12568
+ }
12569
+ });
12570
+ ws.connect();
12571
+ this.connections.push(ws);
12572
+ }
12573
+ log14.info(
12574
+ `WebSocket manager started: ${this.connections.length} connections, ${allStreams.length} streams`
12575
+ );
12576
+ }
12577
+ stop() {
12578
+ for (const ws of this.connections) {
12579
+ ws.close();
12580
+ }
12581
+ this.connections = [];
12582
+ this.started = false;
12583
+ log14.info("WebSocket manager stopped");
12584
+ }
12585
+ getLatestPrice(symbol) {
12586
+ const upper = symbol.toUpperCase().replace("/", "");
12587
+ const cached = this.priceCache.get(upper);
12588
+ if (!cached) return null;
12589
+ if (Date.now() - cached.time > 6e4) return null;
12590
+ return cached.price;
12591
+ }
12592
+ onTrade(symbol, callback) {
12593
+ const upper = symbol.toUpperCase();
12594
+ const set = this.tradeCallbacks.get(upper) ?? /* @__PURE__ */ new Set();
12595
+ set.add(callback);
12596
+ this.tradeCallbacks.set(upper, set);
12597
+ }
12598
+ onKline(symbol, interval, callback) {
12599
+ const key = `${symbol.toUpperCase()}:${interval}`;
12600
+ const set = this.klineCallbacks.get(key) ?? /* @__PURE__ */ new Set();
12601
+ set.add(callback);
12602
+ this.klineCallbacks.set(key, set);
12603
+ }
12604
+ };
12605
+ instance2 = null;
12606
+ }
12607
+ });
12608
+
10752
12609
  // src/tui/components/status-bar.tsx
10753
12610
  import React, { useState, useEffect } from "react";
10754
12611
  import { Box, Text, Spacer } from "ink";
@@ -10873,7 +12730,7 @@ function WelcomeBanner() {
10873
12730
  /* @__PURE__ */ jsxs3(Box3, { children: [
10874
12731
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: " " }),
10875
12732
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "blue", children: "vizzor" }),
10876
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: " v0.10.0" }),
12733
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: " v0.11.0" }),
10877
12734
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " \u2014 AI-powered crypto chronovisor" })
10878
12735
  ] }),
10879
12736
  /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " ML models: LSTM + Random Forest + Isolation Forest + GBM Rug + Wallet LSTM + DistilBERT NLP" }) }),
@@ -11561,6 +13418,8 @@ async function executeCommand(name, args2) {
11561
13418
  return handleConfig(args2);
11562
13419
  case "agent":
11563
13420
  return handleAgent(args2);
13421
+ case "backtest":
13422
+ return handleBacktest(args2);
11564
13423
  case "clear":
11565
13424
  return { blocks: [], text: "" };
11566
13425
  case "exit":
@@ -11910,6 +13769,7 @@ function handleHelp() {
11910
13769
  " /trends Live trending tokens + top gainers/losers",
11911
13770
  " /audit <contract> [--chain <chain>] Audit a smart contract (bytecode scanning)",
11912
13771
  " /agent <sub> Manage autonomous trading agents",
13772
+ " /backtest -s <strat> --pair <P> --from <D> --to <D> Run a historical backtest",
11913
13773
  " /provider [list|<name>] Show/switch AI provider",
11914
13774
  " /config [set <key> <value>] Show or update configuration",
11915
13775
  " /clear Clear message history",
@@ -11922,6 +13782,62 @@ function handleHelp() {
11922
13782
  ].join("\n");
11923
13783
  return { blocks: [], text };
11924
13784
  }
13785
+ async function handleBacktest(args2) {
13786
+ const stratIdx = args2.indexOf("-s") !== -1 ? args2.indexOf("-s") : args2.indexOf("--strategy");
13787
+ const pairIdx = args2.indexOf("--pair");
13788
+ const fromIdx = args2.indexOf("--from");
13789
+ const toIdx = args2.indexOf("--to");
13790
+ const tfIdx = args2.indexOf("--timeframe");
13791
+ const strategy = stratIdx !== -1 && stratIdx + 1 < args2.length ? args2[stratIdx + 1] : "";
13792
+ const pair = pairIdx !== -1 && pairIdx + 1 < args2.length ? args2[pairIdx + 1] : "";
13793
+ const from = fromIdx !== -1 && fromIdx + 1 < args2.length ? args2[fromIdx + 1] : "";
13794
+ const to = toIdx !== -1 && toIdx + 1 < args2.length ? args2[toIdx + 1] : "";
13795
+ const timeframe = tfIdx !== -1 && tfIdx + 1 < args2.length ? args2[tfIdx + 1] : "4h";
13796
+ if (!strategy || !pair || !from || !to) {
13797
+ return {
13798
+ blocks: [],
13799
+ text: "Usage: /backtest -s <strategy> --pair <PAIR> --from <YYYY-MM-DD> --to <YYYY-MM-DD> [--timeframe <tf>]"
13800
+ };
13801
+ }
13802
+ try {
13803
+ const { BacktestEngine: BacktestEngine2 } = await Promise.resolve().then(() => (init_engine2(), engine_exports));
13804
+ const engine = new BacktestEngine2({
13805
+ strategy,
13806
+ pair,
13807
+ from,
13808
+ to,
13809
+ initialCapital: 1e4,
13810
+ timeframe,
13811
+ slippageBps: 10,
13812
+ commissionPct: 0.1
13813
+ });
13814
+ const result = await engine.run();
13815
+ const lines = [
13816
+ `Backtest: ${result.config.strategy} on ${result.config.pair} (${result.config.from} \u2192 ${result.config.to})`,
13817
+ `Initial Capital: $${result.config.initialCapital.toLocaleString()}`,
13818
+ "",
13819
+ `Total Return: $${result.metrics.totalReturn.toFixed(2)} (${result.metrics.totalReturnPct.toFixed(2)}%)`,
13820
+ `Win Rate: ${(result.metrics.winRate * 100).toFixed(1)}%`,
13821
+ `Total Trades: ${result.metrics.totalTrades}`,
13822
+ `Profit Factor: ${result.metrics.profitFactor === Infinity ? "\u221E" : result.metrics.profitFactor.toFixed(2)}`,
13823
+ `Sharpe Ratio: ${result.metrics.sharpeRatio.toFixed(2)}`,
13824
+ `Max Drawdown: ${result.metrics.maxDrawdown.toFixed(2)}%`
13825
+ ];
13826
+ if (result.trades.length > 0) {
13827
+ lines.push("", "Last 5 Trades:");
13828
+ for (const t of result.trades.slice(-5)) {
13829
+ const dir = t.pnl >= 0 ? "+" : "";
13830
+ lines.push(
13831
+ ` ${new Date(t.entryTime).toISOString().split("T")[0]} \u2192 ${new Date(t.exitTime).toISOString().split("T")[0]} | ${t.side} | $${t.entryPrice.toFixed(2)} \u2192 $${t.exitPrice.toFixed(2)} | ${dir}$${t.pnl.toFixed(2)} (${dir}${t.pnlPct.toFixed(2)}%)`
13832
+ );
13833
+ }
13834
+ }
13835
+ return { blocks: [], text: lines.join("\n") };
13836
+ } catch (err) {
13837
+ const message = err instanceof Error ? err.message : String(err);
13838
+ return { blocks: [], text: `Backtest failed: ${message}` };
13839
+ }
13840
+ }
11925
13841
  async function handleConfig(args2) {
11926
13842
  if (args2[0] === "set") {
11927
13843
  const key = args2[1];
@@ -12503,12 +14419,12 @@ var init_app = __esm({
12503
14419
  // src/index.ts
12504
14420
  init_loader();
12505
14421
  import { Command } from "commander";
12506
- import { readFileSync as readFileSync3 } from "fs";
14422
+ import { readFileSync as readFileSync4 } from "fs";
12507
14423
  import { fileURLToPath as fileURLToPath2 } from "url";
12508
14424
  import { dirname as dirname2, resolve as resolve3 } from "path";
12509
14425
  var __filename2 = fileURLToPath2(import.meta.url);
12510
14426
  var __dirname2 = dirname2(__filename2);
12511
- var pkg = JSON.parse(readFileSync3(resolve3(__dirname2, "..", "package.json"), "utf-8"));
14427
+ var pkg = JSON.parse(readFileSync4(resolve3(__dirname2, "..", "package.json"), "utf-8"));
12512
14428
  var program = new Command().name("vizzor").description("Crypto chronovisor \u2014 AI-powered on-chain intelligence").version(pkg.version);
12513
14429
  program.hook("preAction", async () => {
12514
14430
  await loadConfig();
@@ -12583,9 +14499,30 @@ apiKeyCmd.command("revoke <id>").description("Revoke an API key").action(async (
12583
14499
  const { handleApiKeyRevoke: handleApiKeyRevoke2 } = await Promise.resolve().then(() => (init_api(), api_exports));
12584
14500
  handleApiKeyRevoke2(id);
12585
14501
  });
14502
+ apiCmd.command("start").description("Start the REST API server").option("--port <port>", "Server port", (v) => parseInt(v, 10), 3e3).option("--host <host>", "Server host", "0.0.0.0").option("--auth", "Enable API key authentication", false).action(async (options) => {
14503
+ const { handleServe: handleServe2 } = await Promise.resolve().then(() => (init_serve(), serve_exports));
14504
+ await handleServe2(options);
14505
+ });
14506
+ var { registerBacktestCommand: registerBacktestCommand2 } = await Promise.resolve().then(() => (init_backtest2(), backtest_exports));
14507
+ registerBacktestCommand2(program);
14508
+ var { registerWalletCommand: registerWalletCommand2 } = await Promise.resolve().then(() => (init_wallet2(), wallet_exports));
14509
+ registerWalletCommand2(program);
12586
14510
  var args = process.argv.slice(2);
12587
14511
  if (args.length === 0) {
12588
14512
  await loadConfig();
14513
+ const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_loader(), loader_exports));
14514
+ try {
14515
+ const config2 = getConfig2();
14516
+ if (config2.realtime?.enabled) {
14517
+ const { initWSManager: initWSManager2 } = await Promise.resolve().then(() => (init_ws_manager(), ws_manager_exports));
14518
+ const wsManager = initWSManager2();
14519
+ for (const symbol of config2.realtime.symbols) {
14520
+ wsManager.subscribe(symbol);
14521
+ }
14522
+ wsManager.start();
14523
+ }
14524
+ } catch {
14525
+ }
12589
14526
  const { startTUI: startTUI2 } = await Promise.resolve().then(() => (init_app(), app_exports));
12590
14527
  startTUI2();
12591
14528
  } else {