solana-privacy-scanner 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +643 -140
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
  import * as dotenv from "dotenv";
6
6
 
7
- // src/commands/wallet.js
7
+ // src/commands/wallet.ts
8
8
  import { writeFileSync } from "fs";
9
9
 
10
10
  // ../core/dist/index.js
@@ -12,6 +12,8 @@ import { Connection } from "@solana/web3.js";
12
12
  import { readFileSync } from "fs";
13
13
  import { fileURLToPath } from "url";
14
14
  import { dirname, join } from "path";
15
+ var DEFAULT_RPC_URL = "https://late-hardworking-waterfall.solana-mainnet.quiknode.pro/4017b48acf3a2a1665603cac096822ce4bec3a90/";
16
+ var VERSION = "0.4.0";
15
17
  var RateLimiter = class {
16
18
  constructor(maxConcurrency) {
17
19
  this.maxConcurrency = maxConcurrency;
@@ -52,8 +54,8 @@ var RPCClient = class {
52
54
  config;
53
55
  rateLimiter;
54
56
  constructor(configOrUrl) {
55
- const config2 = typeof configOrUrl === "string" ? { rpcUrl: configOrUrl } : configOrUrl;
56
- const rpcUrl = config2.rpcUrl.trim();
57
+ const config2 = !configOrUrl ? {} : typeof configOrUrl === "string" ? { rpcUrl: configOrUrl } : configOrUrl;
58
+ const rpcUrl = (config2.rpcUrl || DEFAULT_RPC_URL).trim();
57
59
  this.config = {
58
60
  maxRetries: config2.maxRetries ?? 3,
59
61
  retryDelay: config2.retryDelay ?? 1e3,
@@ -1093,8 +1095,8 @@ function detectAmountReuse(context) {
1093
1095
  severity: "LOW",
1094
1096
  category: "behavioral",
1095
1097
  reason: `${roundNumbers.length} round-number transfers detected (e.g., 1 SOL, 10 SOL).`,
1096
- impact: "Round numbers are common on Solana and relatively benign alone. Combined with other patterns, they can contribute to fingerprinting.",
1097
- mitigation: "Vary amounts slightly if possible, but this is low priority on Solana.",
1098
+ impact: "Round numbers are common on Solana. Combined with other patterns, they can contribute to fingerprinting.",
1099
+ mitigation: "Vary amounts slightly if possible, but this is low priority.",
1098
1100
  evidence: [{
1099
1101
  description: `${roundNumbers.length} round-number transfers: ${roundNumbers.slice(0, 5).join(", ")}...`,
1100
1102
  severity: "LOW",
@@ -1147,7 +1149,7 @@ function detectAmountReuse(context) {
1147
1149
  severity: "LOW",
1148
1150
  category: "behavioral",
1149
1151
  reason: `${signerReuse.length} amount(s) are reused multiple times with consistent signers.`,
1150
- impact: "Amount reuse alone is relatively weak on Solana, but combined with other signals it contributes to behavioral fingerprinting.",
1152
+ impact: "Amount reuse alone is relatively weak, but combined with other signals it contributes to behavioral fingerprinting.",
1151
1153
  mitigation: "Vary transaction amounts to reduce pattern visibility.",
1152
1154
  evidence
1153
1155
  });
@@ -1179,60 +1181,126 @@ function detectAmountReuse(context) {
1179
1181
  return signals;
1180
1182
  }
1181
1183
  function detectTimingPatterns(context) {
1184
+ const signals = [];
1182
1185
  if (!context.timeRange.earliest || !context.timeRange.latest) {
1183
- return null;
1186
+ return signals;
1184
1187
  }
1185
1188
  if (context.transactionCount < 3) {
1186
- return null;
1189
+ return signals;
1187
1190
  }
1188
1191
  const timeSpanSeconds = context.timeRange.latest - context.timeRange.earliest;
1189
1192
  const timeSpanHours = timeSpanSeconds / 3600;
1190
1193
  if (timeSpanHours === 0) {
1191
- return null;
1194
+ return signals;
1192
1195
  }
1193
1196
  const txRate = context.transactionCount / timeSpanHours;
1194
- let severity = "LOW";
1195
1197
  let isBurst = false;
1198
+ let burstSeverity = "LOW";
1196
1199
  if (txRate > 10) {
1197
- severity = "HIGH";
1200
+ burstSeverity = "HIGH";
1198
1201
  isBurst = true;
1199
1202
  } else if (txRate > 5) {
1200
- severity = "MEDIUM";
1203
+ burstSeverity = "MEDIUM";
1201
1204
  isBurst = true;
1202
1205
  } else if (timeSpanHours < 1 && context.transactionCount >= 3) {
1203
- severity = "MEDIUM";
1206
+ burstSeverity = "MEDIUM";
1204
1207
  isBurst = true;
1205
1208
  }
1206
- if (!isBurst) {
1207
- return null;
1209
+ if (isBurst) {
1210
+ signals.push({
1211
+ id: "timing-burst",
1212
+ name: "Transaction Burst Pattern",
1213
+ severity: burstSeverity,
1214
+ confidence: 0.8,
1215
+ category: "behavioral",
1216
+ reason: `Concentrated activity: ${context.transactionCount} transactions in ${timeSpanHours.toFixed(1)} hours`,
1217
+ impact: "Concentrated transaction activity creates timing fingerprints that can be used to correlate your transactions and link them to specific events or behaviors.",
1218
+ mitigation: "Spread transactions over longer time periods, use scheduled transactions, or batch operations to reduce timing correlation.",
1219
+ evidence: [{
1220
+ description: `${context.transactionCount} transactions in ${timeSpanHours.toFixed(1)} hours (${txRate.toFixed(2)} tx/hour)`,
1221
+ severity: burstSeverity,
1222
+ reference: void 0
1223
+ }]
1224
+ });
1208
1225
  }
1209
- const evidence = [
1210
- {
1211
- type: "timing",
1212
- description: `${context.transactionCount} transactions in ${timeSpanHours.toFixed(1)} hours`,
1213
- data: {
1214
- transactionCount: context.transactionCount,
1215
- timeSpanHours: timeSpanHours.toFixed(2),
1216
- transactionRate: txRate.toFixed(2)
1226
+ if (context.transactions && context.transactions.length >= 5) {
1227
+ const timestamps = context.transactions.map((tx) => tx.blockTime).filter((time) => time !== void 0).sort((a, b) => a - b);
1228
+ if (timestamps.length >= 5) {
1229
+ const gaps = [];
1230
+ for (let i = 1; i < timestamps.length; i++) {
1231
+ gaps.push(timestamps[i] - timestamps[i - 1]);
1232
+ }
1233
+ const avgGap = gaps.reduce((sum, gap) => sum + gap, 0) / gaps.length;
1234
+ const variance = gaps.reduce((sum, gap) => sum + Math.pow(gap - avgGap, 2), 0) / gaps.length;
1235
+ const stdDev = Math.sqrt(variance);
1236
+ const coefficientOfVariation = stdDev / avgGap;
1237
+ if (coefficientOfVariation < 0.3 && avgGap > 60) {
1238
+ const intervalMinutes = Math.round(avgGap / 60);
1239
+ const intervalHours = avgGap / 3600;
1240
+ let severity = "LOW";
1241
+ if (intervalHours >= 23 && intervalHours <= 25) {
1242
+ severity = "HIGH";
1243
+ } else if (intervalHours >= 0.9 && intervalHours <= 1.1) {
1244
+ severity = "HIGH";
1245
+ } else if (gaps.length >= 10) {
1246
+ severity = "MEDIUM";
1247
+ }
1248
+ signals.push({
1249
+ id: "timing-regular-interval",
1250
+ name: "Regular Transaction Interval",
1251
+ severity,
1252
+ confidence: 0.85,
1253
+ category: "behavioral",
1254
+ reason: `Transactions occur at regular ${intervalMinutes < 60 ? `${intervalMinutes}-minute` : `${intervalHours.toFixed(1)}-hour`} intervals.`,
1255
+ impact: "Regular timing patterns are highly distinctive fingerprints. They suggest automated behavior and can reveal timezone, schedule, or bot configuration.",
1256
+ mitigation: "Add random delays between transactions. Vary the timing to avoid predictable patterns.",
1257
+ evidence: [{
1258
+ description: `${gaps.length} transactions with average ${intervalMinutes}-minute intervals (${(coefficientOfVariation * 100).toFixed(1)}% variation)`,
1259
+ severity,
1260
+ reference: void 0
1261
+ }]
1262
+ });
1217
1263
  }
1218
1264
  }
1219
- ];
1220
- return {
1221
- id: "timing-correlation",
1222
- name: "Transaction Burst Pattern",
1223
- severity,
1224
- reason: `Concentrated activity: ${context.transactionCount} transactions in ${timeSpanHours.toFixed(1)} hours`,
1225
- impact: "Concentrated transaction activity creates timing fingerprints that can be used to correlate your transactions and link them to specific events or behaviors.",
1226
- evidence,
1227
- mitigation: "Spread transactions over longer time periods, use scheduled transactions, or batch operations to reduce timing correlation.",
1228
- confidence: 0.8
1229
- };
1265
+ }
1266
+ if (context.transactions && context.transactions.length >= 10) {
1267
+ const timestamps = context.transactions.map((tx) => tx.blockTime).filter((time) => time !== void 0);
1268
+ if (timestamps.length >= 10) {
1269
+ const hours = timestamps.map((ts) => new Date(ts * 1e3).getUTCHours());
1270
+ const hourCounts = /* @__PURE__ */ new Map();
1271
+ hours.forEach((hour) => {
1272
+ hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1);
1273
+ });
1274
+ const maxCount = Math.max(...Array.from(hourCounts.values()));
1275
+ const concentration = maxCount / hours.length;
1276
+ if (concentration > 0.4) {
1277
+ const mostActiveHours = Array.from(hourCounts.entries()).filter(([_, count]) => count >= maxCount * 0.8).map(([hour]) => hour).sort((a, b) => a - b);
1278
+ signals.push({
1279
+ id: "timing-timezone-pattern",
1280
+ name: "Consistent Time-of-Day Pattern",
1281
+ severity: "MEDIUM",
1282
+ confidence: 0.7,
1283
+ category: "behavioral",
1284
+ reason: `${Math.round(concentration * 100)}% of transactions occur during specific hours (${mostActiveHours.map((h) => `${h}:00`).join(", ")} UTC).`,
1285
+ impact: "Time-of-day patterns can reveal timezone or daily schedule, contributing to identity fingerprinting.",
1286
+ mitigation: "Vary transaction times across different hours of the day. Use scheduled transactions or automation to obscure your timezone.",
1287
+ evidence: [{
1288
+ description: `${maxCount}/${hours.length} transactions during ${mostActiveHours.length} hour(s)`,
1289
+ severity: "MEDIUM",
1290
+ reference: void 0
1291
+ }]
1292
+ });
1293
+ }
1294
+ }
1295
+ }
1296
+ return signals;
1230
1297
  }
1231
1298
  function detectKnownEntityInteraction(context) {
1299
+ const signals = [];
1232
1300
  if (context.labels.size === 0) {
1233
- return null;
1301
+ return signals;
1234
1302
  }
1235
- const entityInteractions = [];
1303
+ const entityTypeGroups = /* @__PURE__ */ new Map();
1236
1304
  for (const [address, label] of context.labels.entries()) {
1237
1305
  let interactionCount = 0;
1238
1306
  const relatedTxs = [];
@@ -1245,99 +1313,241 @@ function detectKnownEntityInteraction(context) {
1245
1313
  }
1246
1314
  }
1247
1315
  if (interactionCount > 0) {
1248
- entityInteractions.push({
1249
- type: "label",
1250
- description: `${interactionCount} interaction(s) with ${label.name} (${label.type})`,
1251
- data: {
1252
- entityName: label.name,
1253
- entityType: label.type,
1254
- address,
1255
- interactionCount,
1256
- transactions: relatedTxs
1257
- },
1258
- reference: address
1316
+ if (!entityTypeGroups.has(label.type)) {
1317
+ entityTypeGroups.set(label.type, []);
1318
+ }
1319
+ entityTypeGroups.get(label.type).push({
1320
+ address,
1321
+ label,
1322
+ count: interactionCount,
1323
+ txs: relatedTxs
1259
1324
  });
1260
1325
  }
1261
1326
  }
1262
- if (entityInteractions.length === 0) {
1263
- return null;
1327
+ if (entityTypeGroups.size === 0) {
1328
+ return signals;
1264
1329
  }
1265
- let severity = "MEDIUM";
1266
- const hasExchangeInteraction = Array.from(context.labels.values()).some((label) => label.type === "exchange");
1267
- if (hasExchangeInteraction) {
1268
- severity = "HIGH";
1269
- } else if (entityInteractions.length >= 3) {
1270
- severity = "HIGH";
1330
+ const exchanges = entityTypeGroups.get("exchange");
1331
+ if (exchanges && exchanges.length > 0) {
1332
+ const evidence = exchanges.map((entity) => ({
1333
+ description: `${entity.count} interaction(s) with ${entity.label.name}`,
1334
+ severity: "HIGH",
1335
+ reference: entity.address
1336
+ }));
1337
+ const totalExchangeTxs = exchanges.reduce((sum, e) => sum + e.count, 0);
1338
+ signals.push({
1339
+ id: "known-entity-exchange",
1340
+ name: "Centralized Exchange Interaction",
1341
+ severity: "HIGH",
1342
+ confidence: 0.95,
1343
+ category: "identity-linkage",
1344
+ reason: `Wallet interacted with ${exchanges.length} centralized exchange(s) in ${totalExchangeTxs} transaction(s).`,
1345
+ impact: "Centralized exchanges have KYC data. Direct interactions can link your on-chain address to your real-world identity through account records, IP addresses, and withdrawal/deposit patterns.",
1346
+ mitigation: "Use intermediate wallets to break the direct link. Deposit to privacy protocols before going to CEX. Consider DEXs for better privacy.",
1347
+ evidence
1348
+ });
1271
1349
  }
1272
- return {
1273
- id: "known-entity-interaction",
1274
- name: "Known Entity Interaction",
1275
- severity,
1276
- reason: `Wallet interacted with ${entityInteractions.length} known entit${entityInteractions.length === 1 ? "y" : "ies"}`,
1277
- impact: "Interactions with centralized exchanges, bridges, or other known entities can link your on-chain address to your real-world identity through KYC data, IP addresses, and off-chain records.",
1278
- evidence: entityInteractions,
1279
- mitigation: "Use privacy-preserving bridges, avoid direct CEX interactions from privacy-sensitive wallets, or use intermediate wallets to break the link.",
1280
- confidence: 0.95
1281
- };
1350
+ const bridges = entityTypeGroups.get("bridge");
1351
+ if (bridges && bridges.length > 0) {
1352
+ const evidence = bridges.map((entity) => ({
1353
+ description: `${entity.count} interaction(s) with ${entity.label.name}`,
1354
+ severity: "MEDIUM",
1355
+ reference: entity.address
1356
+ }));
1357
+ signals.push({
1358
+ id: "known-entity-bridge",
1359
+ name: "Bridge Protocol Interaction",
1360
+ severity: "MEDIUM",
1361
+ confidence: 0.85,
1362
+ category: "identity-linkage",
1363
+ reason: `Wallet interacted with ${bridges.length} bridge protocol(s).`,
1364
+ impact: "Bridge transactions can link your Solana address to addresses on other chains, expanding the tracking surface.",
1365
+ mitigation: "Use privacy-preserving bridges when available. Create separate addresses for cross-chain activity.",
1366
+ evidence
1367
+ });
1368
+ }
1369
+ const others = Array.from(entityTypeGroups.entries()).filter(([type]) => type !== "exchange" && type !== "bridge");
1370
+ if (others.length > 0) {
1371
+ const allOtherEntities = others.flatMap(([_, entities]) => entities);
1372
+ const evidence = allOtherEntities.slice(0, 5).map((entity) => ({
1373
+ description: `${entity.count} interaction(s) with ${entity.label.name} (${entity.label.type})`,
1374
+ severity: "LOW",
1375
+ reference: entity.address
1376
+ }));
1377
+ const totalOtherTxs = allOtherEntities.reduce((sum, e) => sum + e.count, 0);
1378
+ signals.push({
1379
+ id: "known-entity-other",
1380
+ name: "Known Entity Interactions",
1381
+ severity: "LOW",
1382
+ confidence: 0.75,
1383
+ category: "behavioral",
1384
+ reason: `Wallet interacted with ${allOtherEntities.length} known entit${allOtherEntities.length === 1 ? "y" : "ies"} (${totalOtherTxs} transactions).`,
1385
+ impact: "Interactions with known entities create reference points in your transaction history. These can be used to correlate activity and build behavioral profiles.",
1386
+ mitigation: "While interacting with known protocols is often necessary, be aware it creates public association with those services.",
1387
+ evidence
1388
+ });
1389
+ }
1390
+ for (const [address, label] of context.labels.entries()) {
1391
+ let interactionCount = 0;
1392
+ for (const transfer of context.transfers) {
1393
+ if (transfer.from === address || transfer.to === address) {
1394
+ interactionCount++;
1395
+ }
1396
+ }
1397
+ const concentration = interactionCount / context.transfers.length;
1398
+ if (concentration > 0.3 && interactionCount >= 5) {
1399
+ signals.push({
1400
+ id: `known-entity-frequent-${address.slice(0, 8)}`,
1401
+ name: "Frequent Single Entity Interaction",
1402
+ severity: label.type === "exchange" ? "HIGH" : "MEDIUM",
1403
+ confidence: 0.85,
1404
+ category: "behavioral",
1405
+ reason: `${Math.round(concentration * 100)}% of transfers (${interactionCount}/${context.transfers.length}) involve ${label.name}.`,
1406
+ impact: "Heavy concentration of activity with one entity creates a strong link and behavioral dependency that is easily identified.",
1407
+ mitigation: "Diversify your interactions across multiple services. Use different addresses for different service providers.",
1408
+ evidence: [{
1409
+ description: `${interactionCount} transfers with ${label.name} (${label.type})`,
1410
+ severity: label.type === "exchange" ? "HIGH" : "MEDIUM",
1411
+ reference: address
1412
+ }]
1413
+ });
1414
+ }
1415
+ }
1416
+ return signals;
1282
1417
  }
1283
1418
  function detectBalanceTraceability(context) {
1419
+ const signals = [];
1284
1420
  if (context.targetType !== "wallet" || context.transfers.length < 2) {
1285
- return null;
1421
+ return signals;
1286
1422
  }
1287
- const fullBalanceTransfers = [];
1288
- const suspiciousPatterns = [];
1289
1423
  const amountPairs = /* @__PURE__ */ new Map();
1290
1424
  for (const transfer of context.transfers) {
1291
1425
  const amountKey = transfer.amount.toFixed(6);
1292
1426
  amountPairs.set(amountKey, (amountPairs.get(amountKey) || 0) + 1);
1293
1427
  }
1294
- const matchingPairs = Array.from(amountPairs.entries()).filter(([_, count]) => count >= 2);
1428
+ const matchingPairs = Array.from(amountPairs.entries()).filter(([_, count]) => count >= 2).sort((a, b) => b[1] - a[1]);
1295
1429
  if (matchingPairs.length >= 2) {
1296
- suspiciousPatterns.push("Multiple matching send/receive amounts detected");
1430
+ const evidence = matchingPairs.slice(0, 5).map(([amount, count]) => ({
1431
+ description: `Amount ${amount} appears in ${count} transfers`,
1432
+ severity: count >= 4 ? "HIGH" : "MEDIUM",
1433
+ reference: void 0
1434
+ }));
1435
+ const severity = matchingPairs.length >= 4 ? "HIGH" : matchingPairs.length >= 3 ? "MEDIUM" : "LOW";
1436
+ signals.push({
1437
+ id: "balance-matching-pairs",
1438
+ name: "Matching Send/Receive Amounts",
1439
+ severity,
1440
+ confidence: 0.7,
1441
+ category: "traceability",
1442
+ reason: `${matchingPairs.length} amount(s) appear in multiple transfers, suggesting balance movements.`,
1443
+ impact: "Matching amounts can be used to trace balance flows. If you receive X and later send X, observers can link these transactions.",
1444
+ mitigation: "Split large transfers into multiple smaller ones with varied amounts. Avoid sending exact amounts you received.",
1445
+ evidence
1446
+ });
1297
1447
  }
1448
+ const sequentialPairs = [];
1298
1449
  for (let i = 0; i < context.transfers.length - 1; i++) {
1299
1450
  const current = context.transfers[i];
1300
1451
  const next = context.transfers[i + 1];
1301
1452
  if (current.blockTime && next.blockTime) {
1302
1453
  const timeDiff = Math.abs(next.blockTime - current.blockTime);
1303
- if (timeDiff < 3600 && Math.abs(current.amount - next.amount) < current.amount * 0.1) {
1304
- suspiciousPatterns.push("Sequential transfers of similar amounts");
1305
- break;
1454
+ const amountDiff = Math.abs(current.amount - next.amount);
1455
+ const percentDiff = amountDiff / Math.max(current.amount, next.amount);
1456
+ if (timeDiff < 3600 && percentDiff < 0.1 && current.amount > 0.1) {
1457
+ sequentialPairs.push({
1458
+ index: i,
1459
+ amount1: current.amount,
1460
+ amount2: next.amount,
1461
+ timeDiff
1462
+ });
1306
1463
  }
1307
1464
  }
1308
1465
  }
1309
- if (suspiciousPatterns.length === 0 && matchingPairs.length === 0) {
1310
- return null;
1311
- }
1312
- const evidence = [];
1313
- if (matchingPairs.length > 0) {
1314
- evidence.push({
1315
- type: "pattern",
1316
- description: `${matchingPairs.length} matching send/receive amount pair(s)`,
1317
- data: { matchingPairs: matchingPairs.length }
1466
+ if (sequentialPairs.length >= 2) {
1467
+ const evidence = sequentialPairs.slice(0, 3).map((pair) => ({
1468
+ description: `${pair.amount1.toFixed(4)} \u2192 ${pair.amount2.toFixed(4)} (${Math.round(pair.timeDiff / 60)} minutes apart)`,
1469
+ severity: pair.timeDiff < 600 ? "HIGH" : "MEDIUM",
1470
+ reference: void 0
1471
+ }));
1472
+ signals.push({
1473
+ id: "balance-sequential-similar",
1474
+ name: "Sequential Similar Amount Transfers",
1475
+ severity: "MEDIUM",
1476
+ confidence: 0.65,
1477
+ category: "traceability",
1478
+ reason: `${sequentialPairs.length} instance(s) of similar amounts transferred in quick succession.`,
1479
+ impact: "Sequential similar amounts suggest balance movements and make it easy to trace funds through intermediate addresses.",
1480
+ mitigation: "Add random delays between transactions. Vary amounts to obscure the flow path.",
1481
+ evidence
1318
1482
  });
1319
1483
  }
1320
- for (const pattern of suspiciousPatterns) {
1321
- evidence.push({
1322
- type: "pattern",
1323
- description: pattern,
1324
- data: {}
1484
+ const roundNumbers = context.transfers.filter((t) => {
1485
+ const amount = t.amount;
1486
+ if (amount === 0) return false;
1487
+ return (amount === Math.floor(amount) || // Whole number
1488
+ amount * 10 === Math.floor(amount * 10) || // One decimal
1489
+ amount * 100 === Math.floor(amount * 100)) && (amount % 1 === 0 || // 1, 10, 100
1490
+ amount * 10 % 1 === 0 || // 0.1, 0.5
1491
+ amount * 100 % 1 === 0);
1492
+ });
1493
+ const roundNumberRatio = roundNumbers.length / context.transfers.length;
1494
+ if (roundNumberRatio > 0.7 && context.transfers.length >= 5) {
1495
+ signals.push({
1496
+ id: "balance-round-numbers",
1497
+ name: "High Proportion of Round Number Transfers",
1498
+ severity: "LOW",
1499
+ confidence: 0.6,
1500
+ category: "behavioral",
1501
+ reason: `${Math.round(roundNumberRatio * 100)}% of transfers use round numbers.`,
1502
+ impact: "Round numbers are easier to remember and track. They can contribute to balance traceability when combined with other patterns.",
1503
+ mitigation: "Use more varied amounts. Add small random values to make amounts less predictable.",
1504
+ evidence: [{
1505
+ description: `${roundNumbers.length}/${context.transfers.length} transfers are round numbers`,
1506
+ severity: "LOW",
1507
+ reference: void 0
1508
+ }]
1325
1509
  });
1326
1510
  }
1327
- let severity = "MEDIUM";
1328
- if (matchingPairs.length >= 3 || suspiciousPatterns.length >= 2) {
1329
- severity = "HIGH";
1511
+ const tokenTransfers = /* @__PURE__ */ new Map();
1512
+ for (const transfer of context.transfers) {
1513
+ const token = transfer.token || "SOL";
1514
+ if (!tokenTransfers.has(token)) {
1515
+ tokenTransfers.set(token, []);
1516
+ }
1517
+ tokenTransfers.get(token).push(transfer);
1518
+ }
1519
+ for (const [token, transfers] of tokenTransfers) {
1520
+ if (transfers.length < 2) continue;
1521
+ const receives = transfers.filter((t) => t.to === context.target);
1522
+ const sends = transfers.filter((t) => t.from === context.target);
1523
+ for (const receive of receives) {
1524
+ for (const send of sends) {
1525
+ if (!receive.blockTime || !send.blockTime) continue;
1526
+ if (send.blockTime <= receive.blockTime) continue;
1527
+ const timeDiff = send.blockTime - receive.blockTime;
1528
+ const percentDiff = Math.abs(send.amount - receive.amount) / receive.amount;
1529
+ if (percentDiff < 0.05 && timeDiff < 86400 && receive.amount > 1) {
1530
+ signals.push({
1531
+ id: `balance-full-movement-${token}`,
1532
+ name: "Full Balance Movement Detected",
1533
+ severity: "HIGH",
1534
+ confidence: 0.8,
1535
+ category: "traceability",
1536
+ reason: `Received ${receive.amount.toFixed(4)} ${token}, then sent ${send.amount.toFixed(4)} ${token} shortly after.`,
1537
+ impact: "Moving entire received balances makes fund flow trivially traceable. The path from source to destination is clear.",
1538
+ mitigation: "Split received funds before sending. Mix with other funds. Add delays and intermediate steps.",
1539
+ evidence: [{
1540
+ description: `Received ${receive.amount.toFixed(4)} \u2192 Sent ${send.amount.toFixed(4)} (${Math.round(timeDiff / 60)} minutes later)`,
1541
+ severity: "HIGH",
1542
+ reference: void 0
1543
+ }]
1544
+ });
1545
+ break;
1546
+ }
1547
+ }
1548
+ }
1330
1549
  }
1331
- return {
1332
- id: "balance-traceability",
1333
- name: "Balance Traceability",
1334
- severity,
1335
- reason: "Wallet shows patterns that enable balance tracking",
1336
- impact: "Traceable balance movements allow observers to follow funds through the blockchain, linking your transactions and revealing your financial activity.",
1337
- evidence,
1338
- mitigation: "Split large transfers into multiple smaller ones, introduce timing delays, or use privacy protocols that obscure amounts.",
1339
- confidence: 0.7
1340
- };
1550
+ return signals;
1341
1551
  }
1342
1552
  function detectFeePayerReuse(context) {
1343
1553
  const signals = [];
@@ -1857,11 +2067,279 @@ function detectTokenAccountLifecycle(context) {
1857
2067
  }
1858
2068
  return signals;
1859
2069
  }
2070
+ function detectMemoExposure(context) {
2071
+ const signals = [];
2072
+ if (context.transactionCount === 0) {
2073
+ return signals;
2074
+ }
2075
+ if (!context.transactions || context.transactions.length === 0) {
2076
+ return signals;
2077
+ }
2078
+ const MEMO_PROGRAM = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
2079
+ const MEMO_PROGRAM_V1 = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
2080
+ const memoInstructions = context.instructions.filter(
2081
+ (inst) => inst.programId === MEMO_PROGRAM || inst.programId === MEMO_PROGRAM_V1
2082
+ );
2083
+ if (memoInstructions.length === 0) {
2084
+ return signals;
2085
+ }
2086
+ const suspiciousMemos = [];
2087
+ for (const inst of memoInstructions) {
2088
+ if (!inst.data || typeof inst.data !== "string") continue;
2089
+ const memoText = inst.data.trim();
2090
+ if (memoText.length === 0) continue;
2091
+ let severity = "LOW";
2092
+ const patterns = [];
2093
+ if (/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/.test(memoText)) {
2094
+ patterns.push("email address");
2095
+ severity = "HIGH";
2096
+ }
2097
+ if (/https?:\/\/[^\s]+/.test(memoText)) {
2098
+ patterns.push("URL");
2099
+ severity = severity === "HIGH" ? "HIGH" : "MEDIUM";
2100
+ }
2101
+ if (/(\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/.test(memoText)) {
2102
+ patterns.push("phone number");
2103
+ severity = "HIGH";
2104
+ }
2105
+ const capitalizedWords = memoText.match(/\b[A-Z][a-z]+(\s+[A-Z][a-z]+)+\b/g);
2106
+ if (capitalizedWords && capitalizedWords.length > 0) {
2107
+ patterns.push("likely name(s)");
2108
+ severity = severity === "HIGH" ? "HIGH" : "MEDIUM";
2109
+ }
2110
+ if (memoText.length > 50 && !patterns.length) {
2111
+ patterns.push("long descriptive text");
2112
+ severity = "MEDIUM";
2113
+ }
2114
+ if (/invoice|payment|order|transaction|ref|reference|id|bill/i.test(memoText)) {
2115
+ patterns.push("payment reference");
2116
+ severity = severity === "HIGH" ? "HIGH" : "MEDIUM";
2117
+ }
2118
+ if (patterns.length > 0) {
2119
+ suspiciousMemos.push({
2120
+ content: memoText.length > 100 ? memoText.slice(0, 100) + "..." : memoText,
2121
+ signature: inst.signature,
2122
+ severity
2123
+ });
2124
+ }
2125
+ }
2126
+ if (suspiciousMemos.length === 0) {
2127
+ signals.push({
2128
+ id: "memo-usage",
2129
+ name: "Memo Program Usage",
2130
+ severity: "LOW",
2131
+ confidence: 0.6,
2132
+ category: "information-leak",
2133
+ reason: `${memoInstructions.length} transaction(s) use memo program. Memos are permanently visible on-chain.`,
2134
+ impact: "Memo data is public and permanent. Even non-sensitive memos can contribute to behavioral fingerprinting.",
2135
+ mitigation: "Avoid using memos unless necessary. Never include personal information.",
2136
+ evidence: [{
2137
+ description: `${memoInstructions.length} transaction(s) with memos`,
2138
+ severity: "LOW",
2139
+ reference: void 0
2140
+ }]
2141
+ });
2142
+ } else {
2143
+ const highSeverity = suspiciousMemos.filter((m) => m.severity === "HIGH");
2144
+ const mediumSeverity = suspiciousMemos.filter((m) => m.severity === "MEDIUM");
2145
+ if (highSeverity.length > 0) {
2146
+ const evidence = highSeverity.map((memo) => ({
2147
+ description: `"${memo.content}"`,
2148
+ severity: "HIGH",
2149
+ reference: `https://solscan.io/tx/${memo.signature}`
2150
+ }));
2151
+ signals.push({
2152
+ id: "memo-pii-exposure",
2153
+ name: "Personal Information in Memo",
2154
+ severity: "HIGH",
2155
+ confidence: 0.9,
2156
+ category: "information-leak",
2157
+ reason: `${highSeverity.length} memo(s) contain personal identifying information (email, phone, name).`,
2158
+ impact: "CRITICAL: Personal information in memos is permanently public. This can directly link your wallet to your real-world identity.",
2159
+ mitigation: "Never put personal information in memos. Contact addresses involved to stop this practice if possible.",
2160
+ evidence
2161
+ });
2162
+ }
2163
+ if (mediumSeverity.length > 0) {
2164
+ const evidence = mediumSeverity.slice(0, 5).map((memo) => ({
2165
+ description: `"${memo.content}"`,
2166
+ severity: "MEDIUM",
2167
+ reference: `https://solscan.io/tx/${memo.signature}`
2168
+ }));
2169
+ signals.push({
2170
+ id: "memo-descriptive-content",
2171
+ name: "Descriptive Content in Memo",
2172
+ severity: "MEDIUM",
2173
+ confidence: 0.7,
2174
+ category: "information-leak",
2175
+ reason: `${mediumSeverity.length} memo(s) contain descriptive or identifying content.`,
2176
+ impact: "Descriptive memos create behavioral fingerprints and can indirectly reveal identity or transaction purpose.",
2177
+ mitigation: "Minimize memo usage. Use generic or coded references instead of descriptive text.",
2178
+ evidence
2179
+ });
2180
+ }
2181
+ }
2182
+ return signals;
2183
+ }
2184
+ function detectAddressReuse(context) {
2185
+ const signals = [];
2186
+ if (context.targetType !== "wallet" || context.transactionCount < 5) {
2187
+ return signals;
2188
+ }
2189
+ const activityTypes = /* @__PURE__ */ new Set();
2190
+ const activityDetails = /* @__PURE__ */ new Map();
2191
+ const DEFI_PROGRAMS = /* @__PURE__ */ new Set([
2192
+ "JUP",
2193
+ "Raydium",
2194
+ "Orca",
2195
+ "Marinade",
2196
+ "Lido",
2197
+ "Lifinity",
2198
+ "Serum"
2199
+ // Add more as needed
2200
+ ]);
2201
+ const NFT_PROGRAMS = /* @__PURE__ */ new Set([
2202
+ "Magic Eden",
2203
+ "Tensor",
2204
+ "OpenSea",
2205
+ "Metaplex"
2206
+ ]);
2207
+ const GAMING_PROGRAMS = /* @__PURE__ */ new Set([
2208
+ "Star Atlas",
2209
+ "Genopets",
2210
+ "Aurory"
2211
+ ]);
2212
+ const DAO_PROGRAMS = /* @__PURE__ */ new Set([
2213
+ "Realms",
2214
+ "Squads",
2215
+ "Tribeca"
2216
+ ]);
2217
+ for (const inst of context.instructions) {
2218
+ const label = context.labels.get(inst.programId);
2219
+ const programName = label?.name || "";
2220
+ if (DEFI_PROGRAMS.has(programName) || /swap|pool|stake|lend|borrow/i.test(programName)) {
2221
+ activityTypes.add("DeFi");
2222
+ if (!activityDetails.has("DeFi")) {
2223
+ activityDetails.set("DeFi", { count: 0, programs: /* @__PURE__ */ new Set() });
2224
+ }
2225
+ const details = activityDetails.get("DeFi");
2226
+ details.count++;
2227
+ details.programs.add(programName || inst.programId.slice(0, 8));
2228
+ }
2229
+ if (NFT_PROGRAMS.has(programName) || /nft|marketplace|mint/i.test(programName)) {
2230
+ activityTypes.add("NFT");
2231
+ if (!activityDetails.has("NFT")) {
2232
+ activityDetails.set("NFT", { count: 0, programs: /* @__PURE__ */ new Set() });
2233
+ }
2234
+ const details = activityDetails.get("NFT");
2235
+ details.count++;
2236
+ details.programs.add(programName || inst.programId.slice(0, 8));
2237
+ }
2238
+ if (GAMING_PROGRAMS.has(programName) || /game|play/i.test(programName)) {
2239
+ activityTypes.add("Gaming");
2240
+ if (!activityDetails.has("Gaming")) {
2241
+ activityDetails.set("Gaming", { count: 0, programs: /* @__PURE__ */ new Set() });
2242
+ }
2243
+ const details = activityDetails.get("Gaming");
2244
+ details.count++;
2245
+ details.programs.add(programName || inst.programId.slice(0, 8));
2246
+ }
2247
+ if (DAO_PROGRAMS.has(programName) || /dao|governance|vote/i.test(programName)) {
2248
+ activityTypes.add("DAO");
2249
+ if (!activityDetails.has("DAO")) {
2250
+ activityDetails.set("DAO", { count: 0, programs: /* @__PURE__ */ new Set() });
2251
+ }
2252
+ const details = activityDetails.get("DAO");
2253
+ details.count++;
2254
+ details.programs.add(programName || inst.programId.slice(0, 8));
2255
+ }
2256
+ }
2257
+ const hasExchangeInteraction = Array.from(context.labels.values()).some((label) => label.type === "exchange");
2258
+ if (hasExchangeInteraction) {
2259
+ activityTypes.add("Exchange");
2260
+ activityDetails.set("Exchange", {
2261
+ count: context.transfers.filter(
2262
+ (t) => context.labels.has(t.from) || context.labels.has(t.to)
2263
+ ).length,
2264
+ programs: /* @__PURE__ */ new Set(["CEX"])
2265
+ });
2266
+ }
2267
+ const simpleTransfers = context.transfers.filter((t) => {
2268
+ const tx = context.transactions?.find((tx2) => tx2.signature === t.signature);
2269
+ return tx && tx.programs && tx.programs.length <= 2;
2270
+ });
2271
+ if (simpleTransfers.length >= 3) {
2272
+ activityTypes.add("P2P Transfers");
2273
+ activityDetails.set("P2P Transfers", {
2274
+ count: simpleTransfers.length,
2275
+ programs: /* @__PURE__ */ new Set(["Direct"])
2276
+ });
2277
+ }
2278
+ const diversityCount = activityTypes.size;
2279
+ if (diversityCount >= 4) {
2280
+ const evidence = Array.from(activityDetails.entries()).map(([type, details]) => ({
2281
+ description: `${type}: ${details.count} transaction(s) across ${details.programs.size} program(s)`,
2282
+ severity: "HIGH",
2283
+ reference: void 0
2284
+ }));
2285
+ signals.push({
2286
+ id: "address-high-diversity",
2287
+ name: "High Activity Diversity on Single Address",
2288
+ severity: "HIGH",
2289
+ confidence: 0.85,
2290
+ category: "linkability",
2291
+ reason: `This address is used for ${diversityCount} distinct activity types: ${Array.from(activityTypes).join(", ")}.`,
2292
+ impact: "Using one address for multiple unrelated activities links them all together. This creates a comprehensive behavioral profile.",
2293
+ mitigation: "Use separate addresses for different purposes: one for DeFi, one for NFTs, one for DAO participation, etc. This isolates activities and prevents cross-linkage.",
2294
+ evidence
2295
+ });
2296
+ } else if (diversityCount === 3) {
2297
+ const evidence = Array.from(activityDetails.entries()).map(([type, details]) => ({
2298
+ description: `${type}: ${details.count} transaction(s)`,
2299
+ severity: "MEDIUM",
2300
+ reference: void 0
2301
+ }));
2302
+ signals.push({
2303
+ id: "address-moderate-diversity",
2304
+ name: "Moderate Activity Diversity on Single Address",
2305
+ severity: "MEDIUM",
2306
+ confidence: 0.7,
2307
+ category: "linkability",
2308
+ reason: `This address is used for ${diversityCount} activity types: ${Array.from(activityTypes).join(", ")}.`,
2309
+ impact: "Multiple activity types on one address create linkage between otherwise separate behaviors.",
2310
+ mitigation: "Consider using separate addresses for different activities to improve privacy compartmentalization.",
2311
+ evidence
2312
+ });
2313
+ }
2314
+ if (context.timeRange.earliest && context.timeRange.latest) {
2315
+ const timeSpanDays = (context.timeRange.latest - context.timeRange.earliest) / (60 * 60 * 24);
2316
+ if (timeSpanDays > 180 && context.transactionCount > 50) {
2317
+ signals.push({
2318
+ id: "address-long-term-usage",
2319
+ name: "Long-Term Single Address Usage",
2320
+ severity: "MEDIUM",
2321
+ confidence: 0.75,
2322
+ category: "behavioral",
2323
+ reason: `This address has been actively used for ${Math.round(timeSpanDays)} days with ${context.transactionCount} transactions.`,
2324
+ impact: "Long-term address usage accumulates a rich behavioral history. All activities over time are permanently linked.",
2325
+ mitigation: "Periodically rotate to new addresses to compartmentalize different time periods of activity.",
2326
+ evidence: [{
2327
+ description: `${context.transactionCount} transactions over ${Math.round(timeSpanDays)} days`,
2328
+ severity: "MEDIUM",
2329
+ reference: void 0
2330
+ }]
2331
+ });
2332
+ }
2333
+ }
2334
+ return signals;
2335
+ }
1860
2336
  var REPORT_VERSION = "1.0.0";
1861
2337
  var HEURISTICS = [
1862
2338
  // Solana-specific (highest priority)
1863
2339
  detectFeePayerReuse,
1864
2340
  detectSignerOverlap,
2341
+ detectMemoExposure,
2342
+ detectAddressReuse,
1865
2343
  detectKnownEntityInteraction,
1866
2344
  detectCounterpartyReuse,
1867
2345
  detectInstructionFingerprinting,
@@ -1905,15 +2383,24 @@ function generateMitigations(signals) {
1905
2383
  if (signalIds.has("token-account-churn") || signalIds.has("rent-refund-clustering")) {
1906
2384
  mitigations.add("Avoid closing token accounts if privacy is important - the rent refund creates linkage.");
1907
2385
  }
1908
- if (signalIds.has("known-entity-interaction")) {
2386
+ if (signalIds.has("memo-pii-exposure") || signalIds.has("memo-descriptive-content") || signalIds.has("memo-usage")) {
2387
+ mitigations.add("Never include personal information in transaction memos - they are permanently public.");
2388
+ }
2389
+ if (signalIds.has("address-high-diversity") || signalIds.has("address-moderate-diversity") || signalIds.has("address-long-term-usage")) {
2390
+ mitigations.add("Use separate addresses for different activity types to compartmentalize your behavior.");
2391
+ }
2392
+ if (signalIds.has("known-entity-exchange") || signalIds.has("known-entity-bridge") || signalIds.has("known-entity-other") || signalIds.has("known-entity-interaction")) {
1909
2393
  mitigations.add("Avoid direct interactions between privacy-sensitive wallets and KYC services.");
1910
2394
  }
1911
2395
  if (signalIds.has("counterparty-reuse") || signalIds.has("pda-reuse")) {
1912
2396
  mitigations.add("Use different addresses for different counterparties or contexts.");
1913
2397
  }
1914
- if (signalIds.has("timing-correlation") || signalIds.has("balance-traceability")) {
2398
+ if (signalIds.has("timing-burst") || signalIds.has("timing-regular-interval") || signalIds.has("timing-timezone-pattern") || signalIds.has("timing-correlation")) {
1915
2399
  mitigations.add("Introduce timing delays and vary transaction patterns to reduce correlation.");
1916
2400
  }
2401
+ if (signalIds.has("balance-matching-pairs") || signalIds.has("balance-sequential-similar") || signalIds.has("balance-full-movement") || signalIds.has("balance-traceability")) {
2402
+ mitigations.add("Vary transfer amounts and add delays to reduce balance traceability.");
2403
+ }
1917
2404
  if (signalIds.has("amount-reuse")) {
1918
2405
  mitigations.add("Vary transaction amounts to avoid creating fingerprints.");
1919
2406
  }
@@ -1979,7 +2466,24 @@ var StaticLabelProvider = class {
1979
2466
  */
1980
2467
  loadLabels(customPath) {
1981
2468
  try {
1982
- const path = customPath || join(__dirname, "known-addresses.json");
2469
+ let path;
2470
+ if (customPath) {
2471
+ path = customPath;
2472
+ } else {
2473
+ const locations = [
2474
+ join(__dirname, "known-addresses.json"),
2475
+ join(__dirname, "../../..", "known-addresses.json")
2476
+ // From src/labels to repo root
2477
+ ];
2478
+ path = locations.find((loc) => {
2479
+ try {
2480
+ readFileSync(loc, "utf-8");
2481
+ return true;
2482
+ } catch {
2483
+ return false;
2484
+ }
2485
+ }) || locations[0];
2486
+ }
1983
2487
  const data = readFileSync(path, "utf-8");
1984
2488
  const parsed = JSON.parse(data);
1985
2489
  if (!parsed.labels || !Array.isArray(parsed.labels)) {
@@ -2037,7 +2541,6 @@ var StaticLabelProvider = class {
2037
2541
  function createDefaultLabelProvider() {
2038
2542
  return new StaticLabelProvider();
2039
2543
  }
2040
- var VERSION = "0.1.0";
2041
2544
 
2042
2545
  // src/formatter.js
2043
2546
  import chalk from "chalk";
@@ -2181,21 +2684,21 @@ function formatReport(report, noColor = false) {
2181
2684
  return lines.join("\n");
2182
2685
  }
2183
2686
 
2184
- // src/commands/wallet.js
2687
+ // src/commands/wallet.ts
2185
2688
  async function scanWallet(address, options) {
2186
2689
  try {
2187
- if (!options.rpc) {
2188
- console.error("Error: RPC URL is required. Set SOLANA_RPC env var or use --rpc flag.");
2189
- process.exit(1);
2190
- }
2191
2690
  console.error(`Scanning wallet: ${address}`);
2192
- console.error(`Using RPC: ${options.rpc.substring(0, 30)}...`);
2193
2691
  console.error("");
2194
- const client = new RPCClient({
2195
- rpcUrl: options.rpc,
2196
- maxConcurrency: 5,
2197
- maxRetries: 3
2198
- });
2692
+ const client = new RPCClient(
2693
+ options.rpc ? {
2694
+ rpcUrl: options.rpc,
2695
+ maxConcurrency: 5,
2696
+ maxRetries: 3
2697
+ } : {
2698
+ maxConcurrency: 5,
2699
+ maxRetries: 3
2700
+ }
2701
+ );
2199
2702
  console.error("Collecting transaction data...");
2200
2703
  const maxSigs = parseInt(options.maxSignatures || "100", 10);
2201
2704
  const rawData = await collectWalletData(client, address, {
@@ -2232,22 +2735,22 @@ async function scanWallet(address, options) {
2232
2735
  }
2233
2736
  }
2234
2737
 
2235
- // src/commands/transaction.js
2738
+ // src/commands/transaction.ts
2236
2739
  import { writeFileSync as writeFileSync2 } from "fs";
2237
2740
  async function scanTransaction(signature, options) {
2238
2741
  try {
2239
- if (!options.rpc) {
2240
- console.error("Error: RPC URL is required. Set SOLANA_RPC env var or use --rpc flag.");
2241
- process.exit(1);
2242
- }
2243
2742
  console.error(`Scanning transaction: ${signature}`);
2244
- console.error(`Using RPC: ${options.rpc.substring(0, 30)}...`);
2245
2743
  console.error("");
2246
- const client = new RPCClient({
2247
- rpcUrl: options.rpc,
2248
- maxConcurrency: 5,
2249
- maxRetries: 3
2250
- });
2744
+ const client = new RPCClient(
2745
+ options.rpc ? {
2746
+ rpcUrl: options.rpc,
2747
+ maxConcurrency: 5,
2748
+ maxRetries: 3
2749
+ } : {
2750
+ maxConcurrency: 5,
2751
+ maxRetries: 3
2752
+ }
2753
+ );
2251
2754
  console.error("Fetching transaction...");
2252
2755
  const rawData = await collectTransactionData(client, signature);
2253
2756
  if (!rawData.transaction) {
@@ -2283,22 +2786,22 @@ async function scanTransaction(signature, options) {
2283
2786
  }
2284
2787
  }
2285
2788
 
2286
- // src/commands/program.js
2789
+ // src/commands/program.ts
2287
2790
  import { writeFileSync as writeFileSync3 } from "fs";
2288
2791
  async function scanProgram(programId, options) {
2289
2792
  try {
2290
- if (!options.rpc) {
2291
- console.error("Error: RPC URL is required. Set SOLANA_RPC env var or use --rpc flag.");
2292
- process.exit(1);
2293
- }
2294
2793
  console.error(`Scanning program: ${programId}`);
2295
- console.error(`Using RPC: ${options.rpc.substring(0, 30)}...`);
2296
2794
  console.error("");
2297
- const client = new RPCClient({
2298
- rpcUrl: options.rpc,
2299
- maxConcurrency: 5,
2300
- maxRetries: 3
2301
- });
2795
+ const client = new RPCClient(
2796
+ options.rpc ? {
2797
+ rpcUrl: options.rpc,
2798
+ maxConcurrency: 5,
2799
+ maxRetries: 3
2800
+ } : {
2801
+ maxConcurrency: 5,
2802
+ maxRetries: 3
2803
+ }
2804
+ );
2302
2805
  console.error("Collecting program data...");
2303
2806
  const maxAccounts = parseInt(options.maxAccounts || "100", 10);
2304
2807
  const maxTxs = parseInt(options.maxTransactions || "50", 10);
@@ -2341,8 +2844,8 @@ dotenv.config({ path: ".env.local" });
2341
2844
  dotenv.config();
2342
2845
  var program = new Command();
2343
2846
  program.name("solana-privacy-scanner").description("Privacy risk scanner for Solana blockchain").version(VERSION);
2344
- program.command("scan-wallet").alias("wallet").description("Scan a Solana wallet address for privacy risks").argument("<address>", "Wallet address to scan").option("--rpc <url>", "RPC endpoint URL", process.env.SOLANA_RPC).option("--json", "Output as JSON", false).option("--max-signatures <number>", "Maximum number of signatures to fetch", "100").option("--output <file>", "Write output to file").action(scanWallet);
2345
- program.command("scan-transaction").alias("tx").description("Scan a single Solana transaction for privacy risks").argument("<signature>", "Transaction signature to scan").option("--rpc <url>", "RPC endpoint URL", process.env.SOLANA_RPC).option("--json", "Output as JSON", false).option("--output <file>", "Write output to file").action(scanTransaction);
2346
- program.command("scan-program").alias("program").description("Scan a Solana program for privacy risks").argument("<programId>", "Program ID to scan").option("--rpc <url>", "RPC endpoint URL", process.env.SOLANA_RPC).option("--json", "Output as JSON", false).option("--max-accounts <number>", "Maximum number of accounts to fetch", "100").option("--max-transactions <number>", "Maximum number of transactions to fetch", "50").option("--output <file>", "Write output to file").action(scanProgram);
2847
+ program.command("scan-wallet").alias("wallet").description("Scan a Solana wallet address for privacy risks").argument("<address>", "Wallet address to scan").option("--rpc <url>", "Custom RPC endpoint URL (optional)", process.env.SOLANA_RPC).option("--json", "Output as JSON", false).option("--max-signatures <number>", "Maximum number of signatures to fetch", "100").option("--output <file>", "Write output to file").action(scanWallet);
2848
+ program.command("scan-transaction").alias("tx").description("Scan a single Solana transaction for privacy risks").argument("<signature>", "Transaction signature to scan").option("--rpc <url>", "Custom RPC endpoint URL (optional)", process.env.SOLANA_RPC).option("--json", "Output as JSON", false).option("--output <file>", "Write output to file").action(scanTransaction);
2849
+ program.command("scan-program").alias("program").description("Scan a Solana program for privacy risks").argument("<programId>", "Program ID to scan").option("--rpc <url>", "Custom RPC endpoint URL (optional)", process.env.SOLANA_RPC).option("--json", "Output as JSON", false).option("--max-accounts <number>", "Maximum number of accounts to fetch", "100").option("--max-transactions <number>", "Maximum number of transactions to fetch", "50").option("--output <file>", "Write output to file").action(scanProgram);
2347
2850
  program.parse();
2348
2851
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solana-privacy-scanner",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "CLI tool for analyzing Solana wallet privacy and on-chain exposure - scan wallets, transactions, and programs",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "url": "https://github.com/taylorferran/solana-privacy-scanner/issues"
38
38
  },
39
39
  "dependencies": {
40
- "solana-privacy-scanner-core": "^0.1.0",
40
+ "solana-privacy-scanner-core": "^0.3.1",
41
41
  "chalk": "^5.3.0",
42
42
  "commander": "^12.1.0",
43
43
  "dotenv": "^16.4.7"