solana-privacy-scanner-core 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,7 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ ALL_TEMPLATES: () => ALL_TEMPLATES,
34
+ CATEGORY_DEFINITIONS: () => CATEGORY_DEFINITIONS,
33
35
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
36
+ FileNicknameProvider: () => FileNicknameProvider,
37
+ MemoryNicknameProvider: () => MemoryNicknameProvider,
34
38
  PERMISSIVE_CONFIG: () => PERMISSIVE_CONFIG,
35
39
  PrivacyConfigSchema: () => PrivacyConfigSchema,
36
40
  RPCClient: () => RPCClient,
@@ -43,7 +47,10 @@ __export(index_exports, {
43
47
  collectTransactionData: () => collectTransactionData,
44
48
  collectWalletData: () => collectWalletData,
45
49
  compareImplementations: () => compareImplementations,
50
+ createAddressFormatter: () => createAddressFormatter,
46
51
  createDefaultLabelProvider: () => createDefaultLabelProvider,
52
+ createFileNicknameProvider: () => createFileNicknameProvider,
53
+ createMemoryNicknameProvider: () => createMemoryNicknameProvider,
47
54
  detectATALinkage: () => detectATALinkage,
48
55
  detectAddressReuse: () => detectAddressReuse,
49
56
  detectCounterpartyReuse: () => detectCounterpartyReuse,
@@ -52,6 +59,7 @@ __export(index_exports, {
52
59
  detectIdentityMetadataExposure: () => detectIdentityMetadataExposure,
53
60
  detectInstructionFingerprinting: () => detectInstructionFingerprinting,
54
61
  detectKnownEntityInteraction: () => detectKnownEntityInteraction,
62
+ detectLocationInference: () => detectLocationInference,
55
63
  detectMemoExposure: () => detectMemoExposure,
56
64
  detectMemoPII: () => detectMemoPII,
57
65
  detectPriorityFeeFingerprinting: () => detectPriorityFeeFingerprinting,
@@ -59,20 +67,34 @@ __export(index_exports, {
59
67
  detectStakingDelegationPatterns: () => detectStakingDelegationPatterns,
60
68
  detectTimingPatterns: () => detectTimingPatterns,
61
69
  detectTokenAccountLifecycle: () => detectTokenAccountLifecycle,
70
+ determineIdentifiability: () => determineIdentifiability,
71
+ displayAddress: () => displayAddress,
72
+ displayAddresses: () => displayAddresses,
62
73
  evaluateHeuristics: () => evaluateHeuristics,
74
+ findTemplate: () => findTemplate,
75
+ generateConclusion: () => generateConclusion,
76
+ generateNarrative: () => generateNarrative,
77
+ generateNarrativeText: () => generateNarrativeText,
63
78
  generateReport: () => generateReport,
79
+ getAddressDisplayInfo: () => getAddressDisplayInfo,
80
+ getCategoriesInOrder: () => getCategoriesInOrder,
64
81
  getEnvironmentConfig: () => getEnvironmentConfig,
82
+ getIdentifiabilityDescription: () => getIdentifiabilityDescription,
65
83
  getMemoRecommendations: () => getMemoRecommendations,
84
+ getSignalCategory: () => getSignalCategory,
66
85
  getTestWallet: () => getTestWallet,
67
86
  groupIssuesByFile: () => groupIssuesByFile,
68
87
  loadConfig: () => loadConfig,
69
88
  normalizeProgramData: () => normalizeProgramData,
70
89
  normalizeTransactionData: () => normalizeTransactionData,
71
90
  normalizeWalletData: () => normalizeWalletData,
91
+ parseNicknameStore: () => parseNicknameStore,
92
+ serializeNicknameStore: () => serializeNicknameStore,
72
93
  shouldEnforce: () => shouldEnforce,
73
94
  simulateTransactionFlow: () => simulateTransactionFlow,
74
95
  simulateTransactionPrivacy: () => simulateTransactionPrivacy,
75
- sortIssues: () => sortIssues
96
+ sortIssues: () => sortIssues,
97
+ truncateAddress: () => truncateAddress
76
98
  });
77
99
  module.exports = __toCommonJS(index_exports);
78
100
 
@@ -80,9 +102,9 @@ module.exports = __toCommonJS(index_exports);
80
102
  var import_web3 = require("@solana/web3.js");
81
103
 
82
104
  // src/constants.ts
83
- var _RPC_ENCODED = "aHR0cHM6Ly9zZXJlbmUtcm91Z2gtcG9vbC5zb2xhbmEtbWFpbm5ldC5xdWlrbm9kZS5wcm8vYTliM2RkNGRkMzc0MzYwYzQzNzY4YzQyMTI2NmE2ZGNlZDU4MTI3Ny8=";
105
+ var _RPC_ENCODED = "aHR0cHM6Ly9sYXRlLWhhcmR3b3JraW5nLXdhdGVyZmFsbC5zb2xhbmEtbWFpbm5ldC5xdWlrbm9kZS5wcm8vNDAxN2I0OGFjZjNhMmExNjY1NjAzY2FjMDk2ODIyY2U0YmVjM2E5MC8=";
84
106
  var DEFAULT_RPC_URL = /* @__PURE__ */ Buffer.from(_RPC_ENCODED, "base64").toString("utf-8");
85
- var VERSION = "0.6.2";
107
+ var VERSION = "0.7.1";
86
108
 
87
109
  // src/rpc/client.ts
88
110
  var RateLimiter = class {
@@ -433,23 +455,38 @@ var PROGRAM_IDS = {
433
455
  MEMO: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
434
456
  MEMO_V1: "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo"
435
457
  };
458
+ function getAccountKeys(tx) {
459
+ const message = tx?.transaction?.message;
460
+ if (!message) return [];
461
+ if (message.accountKeys && Array.isArray(message.accountKeys)) {
462
+ return message.accountKeys.map(
463
+ (key) => typeof key === "string" ? key : key.pubkey?.toString() || key.toString()
464
+ );
465
+ }
466
+ if (message.staticAccountKeys && Array.isArray(message.staticAccountKeys)) {
467
+ const staticKeys = message.staticAccountKeys.map(
468
+ (key) => typeof key === "string" ? key : key.toString()
469
+ );
470
+ return staticKeys;
471
+ }
472
+ return [];
473
+ }
436
474
  function extractTransactionMetadata(tx, signature) {
437
- if (!tx || !tx.transaction || !tx.transaction.message || !tx.transaction.message.accountKeys) {
475
+ const accountKeys = getAccountKeys(tx);
476
+ if (!tx || !tx.transaction || !tx.transaction.message || accountKeys.length === 0) {
438
477
  return {
439
478
  signature,
440
- blockTime: tx?.blockTime || null,
479
+ blockTime: tx?.blockTime ?? null,
441
480
  feePayer: "unknown",
442
481
  signers: []
443
482
  };
444
483
  }
445
- const feePayer = tx.transaction.message.accountKeys[0];
446
- const feePayerAddress = typeof feePayer === "string" ? feePayer : feePayer.pubkey.toString();
447
- const signers = [];
448
- const accountKeys = tx.transaction.message.accountKeys;
449
- signers.push(feePayerAddress);
450
- if (accountKeys && Array.isArray(accountKeys)) {
451
- for (let i = 1; i < accountKeys.length; i++) {
452
- const key = accountKeys[i];
484
+ const feePayerAddress = accountKeys[0];
485
+ const signers = [feePayerAddress];
486
+ const legacyKeys = tx.transaction.message.accountKeys;
487
+ if (legacyKeys && Array.isArray(legacyKeys)) {
488
+ for (let i = 1; i < legacyKeys.length; i++) {
489
+ const key = legacyKeys[i];
453
490
  const address = typeof key === "string" ? key : key.pubkey?.toString();
454
491
  if (typeof key !== "string" && key.signer) {
455
492
  if (address && !signers.includes(address)) {
@@ -458,26 +495,75 @@ function extractTransactionMetadata(tx, signature) {
458
495
  }
459
496
  }
460
497
  }
498
+ const header = tx.transaction.message.header;
499
+ if (header && header.numRequiredSignatures > 1) {
500
+ for (let i = 1; i < header.numRequiredSignatures && i < accountKeys.length; i++) {
501
+ if (!signers.includes(accountKeys[i])) {
502
+ signers.push(accountKeys[i]);
503
+ }
504
+ }
505
+ }
461
506
  let memo;
462
- const instructions = tx.transaction.message.instructions;
463
- if (instructions && Array.isArray(instructions)) {
464
- for (const instruction of instructions) {
465
- if (!instruction || !instruction.programId) continue;
466
- const programId = instruction.programId.toString();
467
- if (programId === PROGRAM_IDS.MEMO || programId === PROGRAM_IDS.MEMO_V1) {
468
- if ("parsed" in instruction && instruction.parsed) {
469
- const parsed = instruction.parsed;
470
- if (parsed.type === "memo" && typeof parsed.info === "string") {
471
- memo = parsed.info;
507
+ const legacyInstructions = tx.transaction.message.instructions;
508
+ if (legacyInstructions && Array.isArray(legacyInstructions)) {
509
+ for (const instruction of legacyInstructions) {
510
+ if (!instruction) continue;
511
+ if (instruction.programId) {
512
+ const programId = instruction.programId.toString();
513
+ if (programId === PROGRAM_IDS.MEMO || programId === PROGRAM_IDS.MEMO_V1) {
514
+ if ("parsed" in instruction && instruction.parsed) {
515
+ const parsed = instruction.parsed;
516
+ if (parsed.type === "memo" && typeof parsed.info === "string") {
517
+ memo = parsed.info;
518
+ }
519
+ } else if ("data" in instruction && typeof instruction.data === "string") {
520
+ try {
521
+ memo = Buffer.from(instruction.data, "base64").toString("utf8");
522
+ } catch {
523
+ memo = instruction.data;
524
+ }
472
525
  }
473
- } else if ("data" in instruction && typeof instruction.data === "string") {
474
- try {
475
- memo = Buffer.from(instruction.data, "base64").toString("utf8");
476
- } catch {
477
- memo = instruction.data;
526
+ break;
527
+ }
528
+ } else if ("programIdIndex" in instruction && accountKeys.length > 0) {
529
+ const programIdIndex = instruction.programIdIndex;
530
+ if (programIdIndex >= 0 && programIdIndex < accountKeys.length) {
531
+ const programId = accountKeys[programIdIndex];
532
+ if (programId === PROGRAM_IDS.MEMO || programId === PROGRAM_IDS.MEMO_V1) {
533
+ const data = instruction.data;
534
+ if (data) {
535
+ try {
536
+ memo = Buffer.from(data, "base64").toString("utf8");
537
+ } catch {
538
+ memo = data;
539
+ }
540
+ }
541
+ break;
542
+ }
543
+ }
544
+ }
545
+ }
546
+ }
547
+ if (!memo) {
548
+ const compiledInstructions = tx.transaction.message.compiledInstructions;
549
+ if (compiledInstructions && Array.isArray(compiledInstructions) && accountKeys.length > 0) {
550
+ for (const instruction of compiledInstructions) {
551
+ if (!instruction) continue;
552
+ const programIdIndex = instruction.programIdIndex;
553
+ if (programIdIndex >= 0 && programIdIndex < accountKeys.length) {
554
+ const programId = accountKeys[programIdIndex];
555
+ if (programId === PROGRAM_IDS.MEMO || programId === PROGRAM_IDS.MEMO_V1) {
556
+ const data = instruction.data;
557
+ if (data) {
558
+ try {
559
+ memo = Buffer.from(data, "base64").toString("utf8");
560
+ } catch {
561
+ memo = data;
562
+ }
563
+ }
564
+ break;
478
565
  }
479
566
  }
480
- break;
481
567
  }
482
568
  }
483
569
  }
@@ -491,7 +577,7 @@ function extractTransactionMetadata(tx, signature) {
491
577
  }
492
578
  return {
493
579
  signature,
494
- blockTime: tx.blockTime,
580
+ blockTime: tx.blockTime ?? null,
495
581
  feePayer: feePayerAddress,
496
582
  signers,
497
583
  computeUnitsUsed,
@@ -738,35 +824,78 @@ function extractInstructions(tx, signature) {
738
824
  return instructions;
739
825
  }
740
826
  const message = tx.transaction.message;
741
- const allInstructions = message.instructions;
742
- if (!allInstructions || !Array.isArray(allInstructions)) {
743
- return instructions;
744
- }
745
- for (const instruction of allInstructions) {
746
- if (!instruction || !instruction.programId) {
747
- continue;
748
- }
749
- const programId = instruction.programId.toString();
750
- const category = categorizeInstruction(instruction);
751
- let data;
752
- if ("parsed" in instruction) {
753
- data = instruction.parsed;
827
+ const accountKeys = getAccountKeys(tx);
828
+ const legacyInstructions = message.instructions;
829
+ if (legacyInstructions && Array.isArray(legacyInstructions)) {
830
+ for (const instruction of legacyInstructions) {
831
+ if (!instruction) continue;
832
+ if (instruction.programId) {
833
+ const programId = instruction.programId.toString();
834
+ const category = categorizeInstruction(instruction);
835
+ let data;
836
+ if ("parsed" in instruction) {
837
+ data = instruction.parsed;
838
+ }
839
+ const accounts = [];
840
+ if ("accounts" in instruction && Array.isArray(instruction.accounts)) {
841
+ for (const acc of instruction.accounts) {
842
+ const address = typeof acc === "string" ? acc : acc.toString();
843
+ accounts.push(address);
844
+ }
845
+ }
846
+ instructions.push({
847
+ programId,
848
+ category,
849
+ signature,
850
+ blockTime: tx.blockTime,
851
+ data,
852
+ accounts: accounts.length > 0 ? accounts : void 0
853
+ });
854
+ } else if ("programIdIndex" in instruction && accountKeys.length > 0) {
855
+ const programIdIndex = instruction.programIdIndex;
856
+ if (programIdIndex >= 0 && programIdIndex < accountKeys.length) {
857
+ const programId = accountKeys[programIdIndex];
858
+ const accounts = [];
859
+ const accountIndices = instruction.accounts || instruction.accountKeyIndexes || [];
860
+ for (const idx of accountIndices) {
861
+ if (idx >= 0 && idx < accountKeys.length) {
862
+ accounts.push(accountKeys[idx]);
863
+ }
864
+ }
865
+ instructions.push({
866
+ programId,
867
+ category: "program_interaction",
868
+ signature,
869
+ blockTime: tx.blockTime,
870
+ accounts: accounts.length > 0 ? accounts : void 0
871
+ });
872
+ }
873
+ }
754
874
  }
755
- const accounts = [];
756
- if ("accounts" in instruction && Array.isArray(instruction.accounts)) {
757
- for (const acc of instruction.accounts) {
758
- const address = typeof acc === "string" ? acc : acc.toString();
759
- accounts.push(address);
875
+ }
876
+ const compiledInstructions = message.compiledInstructions;
877
+ if (compiledInstructions && Array.isArray(compiledInstructions) && accountKeys.length > 0) {
878
+ for (const instruction of compiledInstructions) {
879
+ if (!instruction) continue;
880
+ const programIdIndex = instruction.programIdIndex;
881
+ if (programIdIndex >= 0 && programIdIndex < accountKeys.length) {
882
+ const programId = accountKeys[programIdIndex];
883
+ const accounts = [];
884
+ const accountIndices = instruction.accountKeyIndexes || [];
885
+ for (const idx of accountIndices) {
886
+ if (idx >= 0 && idx < accountKeys.length) {
887
+ accounts.push(accountKeys[idx]);
888
+ }
889
+ }
890
+ instructions.push({
891
+ programId,
892
+ category: "program_interaction",
893
+ signature,
894
+ blockTime: tx.blockTime,
895
+ accounts: accounts.length > 0 ? accounts : void 0
896
+ });
760
897
  }
761
898
  }
762
- instructions.push({
763
- programId,
764
- category,
765
- signature,
766
- blockTime: tx.blockTime,
767
- data,
768
- accounts: accounts.length > 0 ? accounts : void 0
769
- });
770
899
  }
771
900
  return instructions;
772
901
  }
@@ -823,7 +952,6 @@ function normalizeWalletData(rawData, labelProvider) {
823
952
  }
824
953
  }
825
954
  const counterparties = extractCounterparties(allTransfers, rawData.address);
826
- const labels = labelProvider ? labelProvider.lookupMany(Array.from(counterparties)) : /* @__PURE__ */ new Map();
827
955
  const timeRange = calculateTimeRange(transactions);
828
956
  const tokenAccounts = rawData.tokenAccounts.map((ta) => {
829
957
  try {
@@ -848,6 +976,8 @@ function normalizeWalletData(rawData, labelProvider) {
848
976
  for (const instruction of allInstructions) {
849
977
  programs.add(instruction.programId);
850
978
  }
979
+ const allAddressesToLookup = [...counterparties, ...programs];
980
+ const labels = labelProvider ? labelProvider.lookupMany(allAddressesToLookup) : /* @__PURE__ */ new Map();
851
981
  return {
852
982
  target: rawData.address,
853
983
  targetType: "wallet",
@@ -908,7 +1038,8 @@ function normalizeTransactionData(rawData, labelProvider) {
908
1038
  console.warn(`Failed to normalize transaction ${rawData.signature}:`, error);
909
1039
  }
910
1040
  }
911
- const labels = labelProvider ? labelProvider.lookupMany(Array.from(counterparties)) : /* @__PURE__ */ new Map();
1041
+ const allAddressesToLookup = [...counterparties, ...programs];
1042
+ const labels = labelProvider ? labelProvider.lookupMany(allAddressesToLookup) : /* @__PURE__ */ new Map();
912
1043
  return {
913
1044
  target: rawData.signature,
914
1045
  targetType: "transaction",
@@ -1275,6 +1406,12 @@ function detectKnownEntityInteraction(context) {
1275
1406
  }
1276
1407
  }
1277
1408
  }
1409
+ if (context.programs.has(address)) {
1410
+ const programUsageCount = context.instructions.filter(
1411
+ (instr) => instr.programId === address
1412
+ ).length;
1413
+ interactionCount += programUsageCount || 1;
1414
+ }
1278
1415
  if (interactionCount > 0) {
1279
1416
  if (!entityTypeGroups.has(label.type)) {
1280
1417
  entityTypeGroups.set(label.type, []);
@@ -1329,7 +1466,68 @@ function detectKnownEntityInteraction(context) {
1329
1466
  evidence
1330
1467
  });
1331
1468
  }
1332
- const others = Array.from(entityTypeGroups.entries()).filter(([type]) => type !== "exchange" && type !== "bridge");
1469
+ const feePayers = entityTypeGroups.get("fee-payer");
1470
+ if (feePayers && feePayers.length > 0) {
1471
+ const evidence = feePayers.map((entity) => ({
1472
+ description: `${entity.count} interaction(s) with ${entity.label.name}`,
1473
+ severity: "HIGH",
1474
+ reference: entity.address
1475
+ }));
1476
+ const totalFeePayerTxs = feePayers.reduce((sum, e) => sum + e.count, 0);
1477
+ signals.push({
1478
+ id: "known-entity-fee-payer",
1479
+ name: "Fee Payer / Relay Service Usage",
1480
+ severity: "HIGH",
1481
+ confidence: 0.9,
1482
+ category: "identity-linkage",
1483
+ reason: `This wallet used ${feePayers.length} fee payer or relay service(s) across ${totalFeePayerTxs} transaction(s). These services process your transactions and may log activity.`,
1484
+ impact: "Fee payer services see all transactions they sponsor. If the service keeps logs or requires authentication, your wallet activity could be linked to your identity.",
1485
+ mitigation: "Only use fee payer services that have strong privacy policies. Consider using your own SOL for transaction fees when privacy is critical.",
1486
+ evidence
1487
+ });
1488
+ }
1489
+ const marketplaces = entityTypeGroups.get("marketplace");
1490
+ if (marketplaces && marketplaces.length > 0) {
1491
+ const evidence = marketplaces.map((entity) => ({
1492
+ description: `${entity.count} interaction(s) with ${entity.label.name}`,
1493
+ severity: "MEDIUM",
1494
+ reference: entity.address
1495
+ }));
1496
+ signals.push({
1497
+ id: "known-entity-marketplace",
1498
+ name: "NFT Marketplace Activity",
1499
+ severity: "MEDIUM",
1500
+ confidence: 0.8,
1501
+ category: "behavioral",
1502
+ reason: `This wallet traded on ${marketplaces.length} NFT marketplace(s). NFT transactions reveal collecting interests and can be linked to social profiles.`,
1503
+ impact: "NFT purchases are public and often tied to social accounts (Twitter, Discord). Unique NFT holdings can identify you across platforms.",
1504
+ mitigation: "Use a dedicated wallet for NFT trading. Avoid linking NFT wallets to public social profiles.",
1505
+ evidence
1506
+ });
1507
+ }
1508
+ const privacyProtocols = entityTypeGroups.get("privacy");
1509
+ const mixers = entityTypeGroups.get("mixer");
1510
+ const allPrivacy = [...privacyProtocols || [], ...mixers || []];
1511
+ if (allPrivacy.length > 0) {
1512
+ const evidence = allPrivacy.map((entity) => ({
1513
+ description: `${entity.count} interaction(s) with ${entity.label.name}`,
1514
+ severity: "LOW",
1515
+ reference: entity.address
1516
+ }));
1517
+ signals.push({
1518
+ id: "known-entity-privacy",
1519
+ name: "Privacy Protocol Usage",
1520
+ severity: "LOW",
1521
+ confidence: 0.85,
1522
+ category: "behavioral",
1523
+ reason: `This wallet interacted with ${allPrivacy.length} privacy-enhancing protocol(s). While privacy tools improve anonymity, the interaction itself is publicly visible.`,
1524
+ impact: "Using privacy protocols is a positive step, but observers can see you used these services. Some jurisdictions scrutinize privacy tool usage.",
1525
+ mitigation: "Continue using privacy tools but be aware the interaction is visible. Consider using a separate wallet for privacy protocol interactions.",
1526
+ evidence
1527
+ });
1528
+ }
1529
+ const handledTypes = ["exchange", "bridge", "fee-payer", "marketplace", "privacy", "mixer"];
1530
+ const others = Array.from(entityTypeGroups.entries()).filter(([type]) => !handledTypes.includes(type));
1333
1531
  if (others.length > 0) {
1334
1532
  const allOtherEntities = others.flatMap(([_, entities]) => entities);
1335
1533
  const evidence = allOtherEntities.slice(0, 5).map((entity) => ({
@@ -1359,10 +1557,11 @@ function detectKnownEntityInteraction(context) {
1359
1557
  }
1360
1558
  const concentration = interactionCount / context.transfers.length;
1361
1559
  if (concentration > 0.3 && interactionCount >= 5) {
1560
+ const isHighRiskType = label.type === "exchange" || label.type === "fee-payer";
1362
1561
  signals.push({
1363
1562
  id: `known-entity-frequent-${address.slice(0, 8)}`,
1364
1563
  name: "Frequent Single Entity Interaction",
1365
- severity: label.type === "exchange" ? "HIGH" : "MEDIUM",
1564
+ severity: isHighRiskType ? "HIGH" : "MEDIUM",
1366
1565
  confidence: 0.85,
1367
1566
  category: "behavioral",
1368
1567
  reason: `${Math.round(concentration * 100)}% of this wallet's transfers (${interactionCount} out of ${context.transfers.length}) go to or come from ${label.name}. This makes it easy to see what service you use the most.`,
@@ -1370,7 +1569,7 @@ function detectKnownEntityInteraction(context) {
1370
1569
  mitigation: "Spread your activity across different services, or use a dedicated wallet for each service you use frequently.",
1371
1570
  evidence: [{
1372
1571
  description: `${interactionCount} transfers with ${label.name} (${label.type})`,
1373
- severity: label.type === "exchange" ? "HIGH" : "MEDIUM",
1572
+ severity: isHighRiskType ? "HIGH" : "MEDIUM",
1374
1573
  reference: address
1375
1574
  }]
1376
1575
  });
@@ -2466,6 +2665,238 @@ function detectIdentityMetadataExposure(context) {
2466
2665
  return signals;
2467
2666
  }
2468
2667
 
2668
+ // src/heuristics/location-inference.ts
2669
+ var TIMEZONE_REGIONS = {
2670
+ "UTC-12": { offset: -12, region: "Baker Island, Howland Island" },
2671
+ "UTC-11": { offset: -11, region: "American Samoa, Niue" },
2672
+ "UTC-10": { offset: -10, region: "Hawaii, Tahiti" },
2673
+ "UTC-9": { offset: -9, region: "Alaska" },
2674
+ "UTC-8": { offset: -8, region: "US Pacific (LA, San Francisco)" },
2675
+ "UTC-7": { offset: -7, region: "US Mountain (Denver, Phoenix)" },
2676
+ "UTC-6": { offset: -6, region: "US Central (Chicago, Dallas)" },
2677
+ "UTC-5": { offset: -5, region: "US Eastern (NYC, Miami)" },
2678
+ "UTC-4": { offset: -4, region: "Atlantic (Puerto Rico, Bermuda)" },
2679
+ "UTC-3": { offset: -3, region: "Brazil, Argentina" },
2680
+ "UTC-2": { offset: -2, region: "Mid-Atlantic" },
2681
+ "UTC-1": { offset: -1, region: "Azores, Cape Verde" },
2682
+ "UTC+0": { offset: 0, region: "UK, Portugal, West Africa" },
2683
+ "UTC+1": { offset: 1, region: "Western Europe (Paris, Berlin)" },
2684
+ "UTC+2": { offset: 2, region: "Eastern Europe (Cairo, Athens)" },
2685
+ "UTC+3": { offset: 3, region: "Moscow, Middle East" },
2686
+ "UTC+4": { offset: 4, region: "UAE, Armenia" },
2687
+ "UTC+5": { offset: 5, region: "Pakistan, West Asia" },
2688
+ "UTC+5.5": { offset: 5.5, region: "India, Sri Lanka" },
2689
+ "UTC+6": { offset: 6, region: "Bangladesh, Central Asia" },
2690
+ "UTC+7": { offset: 7, region: "Thailand, Vietnam, Indonesia" },
2691
+ "UTC+8": { offset: 8, region: "China, Singapore, Hong Kong" },
2692
+ "UTC+9": { offset: 9, region: "Japan, Korea" },
2693
+ "UTC+10": { offset: 10, region: "Australia East (Sydney)" },
2694
+ "UTC+11": { offset: 11, region: "Solomon Islands, Vanuatu" },
2695
+ "UTC+12": { offset: 12, region: "New Zealand, Fiji" }
2696
+ };
2697
+ function detectLocationInference(context) {
2698
+ const signals = [];
2699
+ if (!context.transactions || context.transactions.length < 20) {
2700
+ return signals;
2701
+ }
2702
+ const timestamps = context.transactions.map((tx) => tx.blockTime).filter((time) => time !== void 0).sort((a, b) => a - b);
2703
+ if (timestamps.length < 20) {
2704
+ return signals;
2705
+ }
2706
+ const timeSpanDays = (timestamps[timestamps.length - 1] - timestamps[0]) / 86400;
2707
+ if (timeSpanDays < 3) {
2708
+ return signals;
2709
+ }
2710
+ const sleepAnalysis = analyzeSleepGaps(timestamps);
2711
+ if (sleepAnalysis) {
2712
+ signals.push(sleepAnalysis);
2713
+ }
2714
+ const dayOfWeekAnalysis = analyzeDayOfWeekPatterns(timestamps);
2715
+ if (dayOfWeekAnalysis) {
2716
+ signals.push(dayOfWeekAnalysis);
2717
+ }
2718
+ const timezoneEstimate = estimateTimezone(timestamps);
2719
+ if (timezoneEstimate) {
2720
+ signals.push(timezoneEstimate);
2721
+ }
2722
+ return signals;
2723
+ }
2724
+ function analyzeSleepGaps(timestamps) {
2725
+ const dayGroups = /* @__PURE__ */ new Map();
2726
+ timestamps.forEach((ts) => {
2727
+ const date = new Date(ts * 1e3);
2728
+ const dayKey = `${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()}`;
2729
+ if (!dayGroups.has(dayKey)) {
2730
+ dayGroups.set(dayKey, []);
2731
+ }
2732
+ dayGroups.get(dayKey).push(ts);
2733
+ });
2734
+ if (dayGroups.size < 5) {
2735
+ return null;
2736
+ }
2737
+ const gaps = [];
2738
+ for (let i = 1; i < timestamps.length; i++) {
2739
+ const gapSeconds = timestamps[i] - timestamps[i - 1];
2740
+ const gapHours = gapSeconds / 3600;
2741
+ if (gapHours >= 5 && gapHours <= 12) {
2742
+ const startDate = new Date(timestamps[i - 1] * 1e3);
2743
+ const endDate = new Date(timestamps[i] * 1e3);
2744
+ gaps.push({
2745
+ gapHours,
2746
+ startHour: startDate.getUTCHours(),
2747
+ endHour: endDate.getUTCHours()
2748
+ });
2749
+ }
2750
+ }
2751
+ if (gaps.length < 5) {
2752
+ return null;
2753
+ }
2754
+ const gapStartHours = gaps.map((g) => g.startHour);
2755
+ const hourBuckets = /* @__PURE__ */ new Map();
2756
+ gapStartHours.forEach((hour) => {
2757
+ const bucket = Math.round(hour / 2) * 2;
2758
+ hourBuckets.set(bucket, (hourBuckets.get(bucket) || 0) + 1);
2759
+ });
2760
+ let peakBucket = 0;
2761
+ let peakCount = 0;
2762
+ hourBuckets.forEach((count, bucket) => {
2763
+ if (count > peakCount) {
2764
+ peakCount = count;
2765
+ peakBucket = bucket;
2766
+ }
2767
+ });
2768
+ const gapConcentration = peakCount / gaps.length;
2769
+ if (gapConcentration < 0.5) {
2770
+ return null;
2771
+ }
2772
+ const avgGapHours = gaps.reduce((sum, g) => sum + g.gapHours, 0) / gaps.length;
2773
+ const sleepStartUTC = peakBucket;
2774
+ const sleepEndUTC = (peakBucket + Math.round(avgGapHours)) % 24;
2775
+ const estimatedOffset = (23 - sleepStartUTC + 12) % 24 - 12;
2776
+ const evidence = [{
2777
+ description: `${gaps.length} sleep-like gaps detected (avg ${avgGapHours.toFixed(1)} hours)`,
2778
+ severity: "MEDIUM",
2779
+ reference: void 0
2780
+ }, {
2781
+ description: `Gaps typically start around ${sleepStartUTC}:00-${(sleepStartUTC + 2) % 24}:00 UTC`,
2782
+ severity: "MEDIUM",
2783
+ reference: void 0
2784
+ }];
2785
+ return {
2786
+ id: "location-sleep-pattern",
2787
+ name: "Sleep Pattern Detected",
2788
+ severity: "MEDIUM",
2789
+ confidence: 0.6 + gapConcentration * 0.2,
2790
+ // Higher confidence with more consistent gaps
2791
+ category: "behavioral",
2792
+ reason: `Your transaction history shows consistent ${avgGapHours.toFixed(1)}-hour gaps that look like sleep periods. These gaps typically start around ${sleepStartUTC}:00 UTC, suggesting you may be in a timezone around UTC${estimatedOffset >= 0 ? "+" : ""}${estimatedOffset}.`,
2793
+ impact: "Sleep patterns are one of the strongest indicators of geographic location. Combined with other timing data, this can narrow down where you live to a specific timezone.",
2794
+ mitigation: "Schedule some transactions during your typical sleep hours using automation. Even occasional activity during sleep hours breaks this pattern.",
2795
+ evidence
2796
+ };
2797
+ }
2798
+ function analyzeDayOfWeekPatterns(timestamps) {
2799
+ const dayCounts = [0, 0, 0, 0, 0, 0, 0];
2800
+ timestamps.forEach((ts) => {
2801
+ const day = new Date(ts * 1e3).getUTCDay();
2802
+ dayCounts[day]++;
2803
+ });
2804
+ const weekdayCount = dayCounts[1] + dayCounts[2] + dayCounts[3] + dayCounts[4] + dayCounts[5];
2805
+ const weekendCount = dayCounts[0] + dayCounts[6];
2806
+ const weekdayRate = weekdayCount / 5;
2807
+ const weekendRate = weekendCount / 2;
2808
+ const totalTx = timestamps.length;
2809
+ const weekdayRatio = weekdayCount / totalTx;
2810
+ const weekendRatio = weekendCount / totalTx;
2811
+ const rateRatio = weekdayRate > 0 && weekendRate > 0 ? Math.max(weekdayRate / weekendRate, weekendRate / weekdayRate) : 0;
2812
+ if (rateRatio < 2) {
2813
+ return null;
2814
+ }
2815
+ const moreActiveOn = weekdayRate > weekendRate ? "weekdays" : "weekends";
2816
+ const lessActiveOn = weekdayRate > weekendRate ? "weekends" : "weekdays";
2817
+ const evidence = [{
2818
+ description: `${Math.round(weekdayRatio * 100)}% weekday / ${Math.round(weekendRatio * 100)}% weekend activity`,
2819
+ severity: "LOW",
2820
+ reference: void 0
2821
+ }];
2822
+ const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2823
+ const dayBreakdown = dayCounts.map((count, i) => `${dayNames[i]}: ${count}`).join(", ");
2824
+ evidence.push({
2825
+ description: `Daily breakdown: ${dayBreakdown}`,
2826
+ severity: "LOW",
2827
+ reference: void 0
2828
+ });
2829
+ return {
2830
+ id: "location-weekday-pattern",
2831
+ name: "Weekday/Weekend Activity Pattern",
2832
+ severity: "LOW",
2833
+ confidence: 0.65,
2834
+ category: "behavioral",
2835
+ reason: `You are ${rateRatio.toFixed(1)}x more active on ${moreActiveOn} than ${lessActiveOn}. This pattern is consistent with typical work schedules and can indicate your occupation type.`,
2836
+ impact: "Different activity levels on weekdays vs weekends can reveal whether you have a traditional work schedule, shift work, or other lifestyle patterns.",
2837
+ mitigation: "Spread transactions more evenly across the week, or use scheduled transactions to add activity on your less active days.",
2838
+ evidence
2839
+ };
2840
+ }
2841
+ function estimateTimezone(timestamps) {
2842
+ const hourCounts = new Array(24).fill(0);
2843
+ timestamps.forEach((ts) => {
2844
+ const hour = new Date(ts * 1e3).getUTCHours();
2845
+ hourCounts[hour]++;
2846
+ });
2847
+ let minActivityWindow = { start: 0, count: Infinity };
2848
+ for (let start = 0; start < 24; start++) {
2849
+ let windowCount = 0;
2850
+ for (let i = 0; i < 8; i++) {
2851
+ windowCount += hourCounts[(start + i) % 24];
2852
+ }
2853
+ if (windowCount < minActivityWindow.count) {
2854
+ minActivityWindow = { start, count: windowCount };
2855
+ }
2856
+ }
2857
+ const totalTx = timestamps.length;
2858
+ const avgTxPerHour = totalTx / 24;
2859
+ const inactiveWindowAvg = minActivityWindow.count / 8;
2860
+ if (inactiveWindowAvg >= avgTxPerHour * 0.5) {
2861
+ return null;
2862
+ }
2863
+ const inactiveStartUTC = minActivityWindow.start;
2864
+ let estimatedOffset = -inactiveStartUTC;
2865
+ if (estimatedOffset < -12) estimatedOffset += 24;
2866
+ if (estimatedOffset > 12) estimatedOffset -= 24;
2867
+ const offsetKey = estimatedOffset === Math.floor(estimatedOffset) ? `UTC${estimatedOffset >= 0 ? "+" : ""}${estimatedOffset}` : `UTC${estimatedOffset >= 0 ? "+" : ""}${estimatedOffset}`;
2868
+ const timezoneInfo = TIMEZONE_REGIONS[offsetKey];
2869
+ const regionHint = timezoneInfo?.region || "Unknown region";
2870
+ const inactiveRatio = inactiveWindowAvg / avgTxPerHour;
2871
+ const confidence = Math.min(0.85, 0.5 + (1 - inactiveRatio) * 0.4);
2872
+ const activeStartUTC = (minActivityWindow.start + 8) % 24;
2873
+ const activeEndUTC = minActivityWindow.start;
2874
+ const evidence = [{
2875
+ description: `Least active: ${minActivityWindow.start}:00-${(minActivityWindow.start + 8) % 24}:00 UTC (${minActivityWindow.count} tx)`,
2876
+ severity: "HIGH",
2877
+ reference: void 0
2878
+ }, {
2879
+ description: `Most active: ${activeStartUTC}:00-${activeEndUTC}:00 UTC`,
2880
+ severity: "HIGH",
2881
+ reference: void 0
2882
+ }, {
2883
+ description: `Estimated timezone: UTC${estimatedOffset >= 0 ? "+" : ""}${estimatedOffset} (${regionHint})`,
2884
+ severity: "HIGH",
2885
+ reference: void 0
2886
+ }];
2887
+ return {
2888
+ id: "location-timezone-estimate",
2889
+ name: "Geographic Location Inference",
2890
+ severity: "HIGH",
2891
+ confidence,
2892
+ category: "behavioral",
2893
+ reason: `Based on your transaction timing patterns, you appear to be active between ${activeStartUTC}:00-${activeEndUTC}:00 UTC and inactive between ${minActivityWindow.start}:00-${(minActivityWindow.start + 8) % 24}:00 UTC. This suggests a timezone around UTC${estimatedOffset >= 0 ? "+" : ""}${estimatedOffset}, which corresponds to regions like: ${regionHint}.`,
2894
+ impact: "Timezone estimation is a powerful deanonymization technique. Combined with other data (language preferences, protocol usage), it can significantly narrow down your geographic location.",
2895
+ mitigation: "Use transaction scheduling to maintain activity during your typical inactive hours. Random timing offsets make timezone inference much harder.",
2896
+ evidence
2897
+ };
2898
+ }
2899
+
2469
2900
  // src/scanner/index.ts
2470
2901
  var REPORT_VERSION = "1.0.0";
2471
2902
  var HEURISTICS = [
@@ -2483,7 +2914,9 @@ var HEURISTICS = [
2483
2914
  detectPriorityFeeFingerprinting,
2484
2915
  detectStakingDelegationPatterns,
2485
2916
  // Timing analysis
2486
- detectTimingPatterns
2917
+ detectTimingPatterns,
2918
+ // Location inference (requires more data)
2919
+ detectLocationInference
2487
2920
  ];
2488
2921
  function calculateOverallRisk(signals) {
2489
2922
  if (signals.length === 0) {
@@ -2693,6 +3126,247 @@ function createDefaultLabelProvider() {
2693
3126
  return new StaticLabelProvider();
2694
3127
  }
2695
3128
 
3129
+ // src/nicknames/provider.ts
3130
+ var import_fs2 = require("fs");
3131
+ function isValidNicknameStore(obj) {
3132
+ if (!obj || typeof obj !== "object") return false;
3133
+ const store = obj;
3134
+ return store.version === "1.0.0" && typeof store.nicknames === "object" && store.nicknames !== null && typeof store.createdAt === "number" && typeof store.updatedAt === "number";
3135
+ }
3136
+ var MemoryNicknameProvider = class {
3137
+ nicknames;
3138
+ createdAt;
3139
+ updatedAt;
3140
+ constructor(initialStore) {
3141
+ this.nicknames = /* @__PURE__ */ new Map();
3142
+ const now = Date.now();
3143
+ this.createdAt = now;
3144
+ this.updatedAt = now;
3145
+ if (initialStore) {
3146
+ this.import(initialStore, true);
3147
+ }
3148
+ }
3149
+ get(address) {
3150
+ return this.nicknames.get(address) ?? null;
3151
+ }
3152
+ set(address, nickname) {
3153
+ if (!address || !nickname) {
3154
+ throw new Error("Address and nickname are required");
3155
+ }
3156
+ if (address.length < 32 || address.length > 44) {
3157
+ throw new Error("Invalid Solana address length");
3158
+ }
3159
+ const trimmed = nickname.trim().slice(0, 50);
3160
+ if (trimmed.length === 0) {
3161
+ throw new Error("Nickname cannot be empty");
3162
+ }
3163
+ this.nicknames.set(address, trimmed);
3164
+ this.updatedAt = Date.now();
3165
+ }
3166
+ remove(address) {
3167
+ if (this.nicknames.delete(address)) {
3168
+ this.updatedAt = Date.now();
3169
+ }
3170
+ }
3171
+ has(address) {
3172
+ return this.nicknames.has(address);
3173
+ }
3174
+ getAll() {
3175
+ return new Map(this.nicknames);
3176
+ }
3177
+ count() {
3178
+ return this.nicknames.size;
3179
+ }
3180
+ export() {
3181
+ return {
3182
+ version: "1.0.0",
3183
+ nicknames: Object.fromEntries(this.nicknames),
3184
+ createdAt: this.createdAt,
3185
+ updatedAt: this.updatedAt
3186
+ };
3187
+ }
3188
+ import(store, overwrite = false) {
3189
+ if (!isValidNicknameStore(store)) {
3190
+ throw new Error("Invalid nickname store format");
3191
+ }
3192
+ for (const [address, nickname] of Object.entries(store.nicknames)) {
3193
+ if (overwrite || !this.nicknames.has(address)) {
3194
+ if (typeof nickname === "string" && nickname.trim().length > 0) {
3195
+ this.nicknames.set(address, nickname.trim().slice(0, 50));
3196
+ }
3197
+ }
3198
+ }
3199
+ this.updatedAt = Date.now();
3200
+ }
3201
+ clear() {
3202
+ this.nicknames.clear();
3203
+ this.updatedAt = Date.now();
3204
+ }
3205
+ };
3206
+ var FileNicknameProvider = class extends MemoryNicknameProvider {
3207
+ filePath;
3208
+ autoSave;
3209
+ /**
3210
+ * Create a file-based nickname provider
3211
+ * @param filePath - Path to the JSON file
3212
+ * @param autoSave - Whether to automatically save on every change (default: true)
3213
+ */
3214
+ constructor(filePath, autoSave = true) {
3215
+ super();
3216
+ this.filePath = filePath;
3217
+ this.autoSave = autoSave;
3218
+ this.loadFromFile();
3219
+ }
3220
+ /**
3221
+ * Load nicknames from the file
3222
+ */
3223
+ loadFromFile() {
3224
+ if (!(0, import_fs2.existsSync)(this.filePath)) {
3225
+ return;
3226
+ }
3227
+ try {
3228
+ const data = (0, import_fs2.readFileSync)(this.filePath, "utf-8");
3229
+ const parsed = JSON.parse(data);
3230
+ if (isValidNicknameStore(parsed)) {
3231
+ this.import(parsed, true);
3232
+ this.createdAt = parsed.createdAt;
3233
+ } else {
3234
+ console.warn(`Invalid nickname store format in ${this.filePath}`);
3235
+ }
3236
+ } catch (error) {
3237
+ console.warn(`Failed to load nicknames from ${this.filePath}:`, error);
3238
+ }
3239
+ }
3240
+ /**
3241
+ * Save nicknames to the file
3242
+ */
3243
+ save() {
3244
+ try {
3245
+ const store = this.export();
3246
+ (0, import_fs2.writeFileSync)(this.filePath, JSON.stringify(store, null, 2), "utf-8");
3247
+ } catch (error) {
3248
+ console.error(`Failed to save nicknames to ${this.filePath}:`, error);
3249
+ throw error;
3250
+ }
3251
+ }
3252
+ set(address, nickname) {
3253
+ super.set(address, nickname);
3254
+ if (this.autoSave) {
3255
+ this.save();
3256
+ }
3257
+ }
3258
+ remove(address) {
3259
+ super.remove(address);
3260
+ if (this.autoSave) {
3261
+ this.save();
3262
+ }
3263
+ }
3264
+ import(store, overwrite = false) {
3265
+ super.import(store, overwrite);
3266
+ if (this.autoSave) {
3267
+ this.save();
3268
+ }
3269
+ }
3270
+ clear() {
3271
+ super.clear();
3272
+ if (this.autoSave) {
3273
+ this.save();
3274
+ }
3275
+ }
3276
+ /**
3277
+ * Get the file path
3278
+ */
3279
+ getFilePath() {
3280
+ return this.filePath;
3281
+ }
3282
+ };
3283
+ function createFileNicknameProvider(filePath) {
3284
+ return new FileNicknameProvider(filePath);
3285
+ }
3286
+ function createMemoryNicknameProvider() {
3287
+ return new MemoryNicknameProvider();
3288
+ }
3289
+ function parseNicknameStore(json) {
3290
+ const parsed = JSON.parse(json);
3291
+ if (!isValidNicknameStore(parsed)) {
3292
+ throw new Error("Invalid nickname store format");
3293
+ }
3294
+ return parsed;
3295
+ }
3296
+ function serializeNicknameStore(store) {
3297
+ return JSON.stringify(store, null, 2);
3298
+ }
3299
+
3300
+ // src/utils/display-address.ts
3301
+ function truncateAddress(address, chars = 4) {
3302
+ if (!address || address.length <= chars * 2 + 3) {
3303
+ return address;
3304
+ }
3305
+ return `${address.slice(0, chars)}...${address.slice(-chars)}`;
3306
+ }
3307
+ function displayAddress(address, options = {}) {
3308
+ const {
3309
+ nicknames,
3310
+ labels,
3311
+ truncateChars = 4,
3312
+ showAddressSuffix = true,
3313
+ format = "smart"
3314
+ } = options;
3315
+ if (format === "full") {
3316
+ return address;
3317
+ }
3318
+ if (format === "truncated") {
3319
+ return truncateAddress(address, truncateChars);
3320
+ }
3321
+ const nickname = nicknames?.get(address);
3322
+ if (nickname) {
3323
+ if (showAddressSuffix) {
3324
+ return `${nickname} (${truncateAddress(address, truncateChars)})`;
3325
+ }
3326
+ return nickname;
3327
+ }
3328
+ const label = labels?.lookup(address);
3329
+ if (label) {
3330
+ return label.name;
3331
+ }
3332
+ return truncateAddress(address, truncateChars);
3333
+ }
3334
+ function getAddressDisplayInfo(address, nicknames, labels) {
3335
+ const truncated = truncateAddress(address, 4);
3336
+ const nickname = nicknames?.get(address) ?? void 0;
3337
+ const labelData = labels?.lookup(address);
3338
+ let displayText;
3339
+ let type;
3340
+ if (nickname) {
3341
+ displayText = nickname;
3342
+ type = "nickname";
3343
+ } else if (labelData) {
3344
+ displayText = labelData.name;
3345
+ type = "label";
3346
+ } else {
3347
+ displayText = truncated;
3348
+ type = "address";
3349
+ }
3350
+ return {
3351
+ address,
3352
+ displayText,
3353
+ type,
3354
+ truncated,
3355
+ nickname,
3356
+ label: labelData ? {
3357
+ name: labelData.name,
3358
+ type: labelData.type,
3359
+ description: labelData.description
3360
+ } : void 0
3361
+ };
3362
+ }
3363
+ function displayAddresses(addresses, options = {}) {
3364
+ return addresses.map((addr) => displayAddress(addr, options));
3365
+ }
3366
+ function createAddressFormatter(options) {
3367
+ return (address) => displayAddress(address, options);
3368
+ }
3369
+
2696
3370
  // src/analyzer/utils.ts
2697
3371
  var fs = __toESM(require("fs/promises"), 1);
2698
3372
  var path = __toESM(require("path"), 1);
@@ -3590,7 +4264,7 @@ var PERMISSIVE_CONFIG = {
3590
4264
  };
3591
4265
 
3592
4266
  // src/config/loader.ts
3593
- var import_fs2 = require("fs");
4267
+ var import_fs3 = require("fs");
3594
4268
  var import_path2 = require("path");
3595
4269
  function loadConfig(cwd = process.cwd()) {
3596
4270
  const configPaths = [
@@ -3600,9 +4274,9 @@ function loadConfig(cwd = process.cwd()) {
3600
4274
  ];
3601
4275
  for (const configPath of configPaths) {
3602
4276
  const fullPath = (0, import_path2.join)(cwd, configPath);
3603
- if ((0, import_fs2.existsSync)(fullPath)) {
4277
+ if ((0, import_fs3.existsSync)(fullPath)) {
3604
4278
  try {
3605
- const content = (0, import_fs2.readFileSync)(fullPath, "utf-8");
4279
+ const content = (0, import_fs3.readFileSync)(fullPath, "utf-8");
3606
4280
  const parsed = JSON.parse(content);
3607
4281
  return validateConfig(parsed);
3608
4282
  } catch (error) {
@@ -3613,9 +4287,9 @@ function loadConfig(cwd = process.cwd()) {
3613
4287
  }
3614
4288
  }
3615
4289
  const packageJsonPath = (0, import_path2.join)(cwd, "package.json");
3616
- if ((0, import_fs2.existsSync)(packageJsonPath)) {
4290
+ if ((0, import_fs3.existsSync)(packageJsonPath)) {
3617
4291
  try {
3618
- const content = (0, import_fs2.readFileSync)(packageJsonPath, "utf-8");
4292
+ const content = (0, import_fs3.readFileSync)(packageJsonPath, "utf-8");
3619
4293
  const parsed = JSON.parse(content);
3620
4294
  if (parsed.privacy) {
3621
4295
  return validateConfig(parsed.privacy);
@@ -3663,9 +4337,877 @@ function getTestWallet(config) {
3663
4337
  return config.testWallets.devnet;
3664
4338
  }
3665
4339
  }
4340
+
4341
+ // src/narrative/interpolator.ts
4342
+ function interpolate(template, data) {
4343
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
4344
+ if (key in data) {
4345
+ return String(data[key]);
4346
+ }
4347
+ return match;
4348
+ });
4349
+ }
4350
+ function extractEntityNames(descriptions) {
4351
+ const names = [];
4352
+ for (const desc of descriptions) {
4353
+ const match = desc.match(/with\s+([A-Z][a-zA-Z0-9\s]+?)(?:\s+\(|$|,|\.|:)/);
4354
+ if (match) {
4355
+ names.push(match[1].trim());
4356
+ }
4357
+ }
4358
+ return [...new Set(names)];
4359
+ }
4360
+ function parseCountFromDescription(description) {
4361
+ const match = description.match(/(\d+)\s+(?:transaction|interaction|transfer|time)/i);
4362
+ return match ? parseInt(match[1], 10) : 1;
4363
+ }
4364
+
4365
+ // src/narrative/templates.ts
4366
+ var IDENTITY_TEMPLATES = [
4367
+ // Exchange interactions - highest identity risk
4368
+ {
4369
+ pattern: "known-entity-exchange",
4370
+ category: "identity",
4371
+ template: "I can see that this wallet has directly interacted with {exchangeCount} centralized exchange(s) including {exchangeNames}. Since exchanges require KYC verification, I can potentially link this wallet to a real-world identity by requesting records from these exchanges.",
4372
+ detailTemplates: ["The wallet had {interactionCount} interaction(s) with {exchangeName}."],
4373
+ extractVariables: (signal) => {
4374
+ const names = extractEntityNames(signal.evidence.map((e) => e.description));
4375
+ return {
4376
+ exchangeCount: signal.evidence.length || 1,
4377
+ exchangeNames: names.length > 0 ? names.join(", ") : "known exchanges",
4378
+ interactionCount: parseCountFromDescription(signal.evidence[0]?.description || ""),
4379
+ exchangeName: names[0] || "an exchange"
4380
+ };
4381
+ }
4382
+ },
4383
+ // Bridge interactions - cross-chain tracking
4384
+ {
4385
+ pattern: "known-entity-bridge",
4386
+ category: "identity",
4387
+ template: "I can follow this wallet's bridge transactions to find their addresses on other blockchains. This allows me to correlate activity across multiple chains and build a more complete profile.",
4388
+ extractVariables: (signal) => {
4389
+ const names = extractEntityNames(signal.evidence.map((e) => e.description));
4390
+ return {
4391
+ bridgeCount: signal.evidence.length,
4392
+ bridgeNames: names.join(", ") || "cross-chain bridges"
4393
+ };
4394
+ }
4395
+ },
4396
+ // Domain name linkage - permanent identity link
4397
+ {
4398
+ pattern: "domain-name-linkage",
4399
+ category: "identity",
4400
+ template: "I discovered that this wallet has registered or interacted with a .sol domain name. This creates a direct, permanent link between this cryptographic address and a human-readable identity that anyone can look up.",
4401
+ extractVariables: () => ({})
4402
+ },
4403
+ // NFT metadata exposure - creator identity
4404
+ {
4405
+ pattern: "nft-metadata-exposure",
4406
+ category: "identity",
4407
+ template: "I can see that this wallet has created or updated NFTs via Metaplex. The on-chain metadata permanently associates this wallet with the creator identity, allowing me to identify the owner through their creative work.",
4408
+ extractVariables: (signal) => ({
4409
+ interactionCount: signal.evidence.length
4410
+ })
4411
+ },
4412
+ // Frequent entity interaction (dynamic ID)
4413
+ {
4414
+ pattern: /^known-entity-frequent-/,
4415
+ category: "identity",
4416
+ template: "I notice that {concentration}% of this wallet's activity is concentrated with {entityName}. This heavy usage pattern makes it easy to track this user's habits and potentially correlate with off-chain records.",
4417
+ extractVariables: (signal) => {
4418
+ const match = signal.reason?.match(/(\d+)%/);
4419
+ const names = extractEntityNames([signal.reason || ""]);
4420
+ return {
4421
+ concentration: match?.[1] || "30",
4422
+ entityName: names[0] || "a known service"
4423
+ };
4424
+ }
4425
+ }
4426
+ ];
4427
+ var CONNECTIONS_TEMPLATES = [
4428
+ // Fee payer - never self (critical)
4429
+ {
4430
+ pattern: "fee-payer-never-self",
4431
+ category: "connections",
4432
+ template: "I have identified that this wallet NEVER pays its own transaction fees. All {txCount} transactions were funded by {feePayerCount} other wallet(s). This tells me definitively that someone else controls or funds this account - I can identify the controller by following the fee payments.",
4433
+ extractVariables: (signal) => {
4434
+ const txMatch = signal.reason?.match(/All\s+(\d+)/);
4435
+ const fpMatch = signal.reason?.match(/by\s+(\d+)/);
4436
+ return {
4437
+ txCount: txMatch?.[1] || signal.evidence.length || "?",
4438
+ feePayerCount: fpMatch?.[1] || "1"
4439
+ };
4440
+ }
4441
+ },
4442
+ // Fee payer - external
4443
+ {
4444
+ pattern: "fee-payer-external",
4445
+ category: "connections",
4446
+ template: "I can see that {feePayerCount} external wallet(s) have paid transaction fees for this address. This creates a visible on-chain connection - if I identify any of these fee payers, I automatically link them to this wallet.",
4447
+ extractVariables: (signal) => ({
4448
+ feePayerCount: signal.evidence.length || 1
4449
+ })
4450
+ },
4451
+ // Fee payer - multi signer
4452
+ {
4453
+ pattern: "fee-payer-multi-signer",
4454
+ category: "connections",
4455
+ template: "I have detected that certain fee payers are funding transactions for multiple different signers. This reveals a central operator controlling several wallets - I can now link all these wallets together under one entity.",
4456
+ extractVariables: (signal) => ({
4457
+ operatorCount: signal.evidence.length
4458
+ })
4459
+ },
4460
+ // Signer repeated
4461
+ {
4462
+ pattern: "signer-repeated",
4463
+ category: "connections",
4464
+ template: "I observe that {signerCount} address(es) repeatedly sign transactions for this wallet. This is cryptographic proof that these wallets are connected.",
4465
+ extractVariables: (signal) => {
4466
+ const match = signal.reason?.match(/(\d+)\s+address/);
4467
+ return {
4468
+ signerCount: match?.[1] || signal.evidence.length || "1"
4469
+ };
4470
+ }
4471
+ },
4472
+ // Signer set reuse
4473
+ {
4474
+ pattern: "signer-set-reuse",
4475
+ category: "connections",
4476
+ template: "I can see that the same group of signers is used repeatedly across {patternCount} different transaction patterns. This combination acts as a unique fingerprint identifying your multi-sig setup.",
4477
+ extractVariables: (signal) => ({
4478
+ patternCount: signal.evidence.length
4479
+ })
4480
+ },
4481
+ // Signer authority hub
4482
+ {
4483
+ pattern: "signer-authority-hub",
4484
+ category: "connections",
4485
+ template: 'I have identified {authorityCount} "authority" signer(s) that co-sign transactions with many different wallets. This reveals a central controller - I can map out the entire set of wallets under their control.',
4486
+ extractVariables: (signal) => ({
4487
+ authorityCount: signal.evidence.length
4488
+ })
4489
+ },
4490
+ // ATA creator linkage
4491
+ {
4492
+ pattern: "ata-creator-linkage",
4493
+ category: "connections",
4494
+ template: "I can see that one wallet created token accounts for {ownerCount} different owners. Even though these wallets never send tokens to each other directly, they are permanently linked through their shared funding source.",
4495
+ extractVariables: (signal) => {
4496
+ const match = signal.reason?.match(/for\s+(\d+)\s+different/);
4497
+ return {
4498
+ ownerCount: match?.[1] || signal.evidence.length || "2"
4499
+ };
4500
+ }
4501
+ },
4502
+ // ATA funding pattern
4503
+ {
4504
+ pattern: "ata-funding-pattern",
4505
+ category: "connections",
4506
+ template: "I detected {burstCount} token accounts created within a short time window. This batch setup pattern tells me these wallets were prepared by the same operator.",
4507
+ extractVariables: (signal) => {
4508
+ const match = signal.reason?.match(/(\d+)\s+token\s+accounts/);
4509
+ return {
4510
+ burstCount: match?.[1] || signal.evidence.length || "3"
4511
+ };
4512
+ }
4513
+ },
4514
+ // Counterparty reuse
4515
+ {
4516
+ pattern: "counterparty-reuse",
4517
+ category: "connections",
4518
+ template: "I can map out this wallet's regular contacts - {counterpartyCount} address(es) appear repeatedly in transfers, revealing established relationships.",
4519
+ extractVariables: (signal) => ({
4520
+ counterpartyCount: signal.evidence.length || 1
4521
+ })
4522
+ },
4523
+ // PDA reuse
4524
+ {
4525
+ pattern: "pda-reuse",
4526
+ category: "connections",
4527
+ template: "I can track this wallet's repeated interactions with {pdaCount} program-derived account(s). Each visit to these accounts links those transactions together, revealing a complete activity timeline.",
4528
+ extractVariables: (signal) => ({
4529
+ pdaCount: signal.evidence.length
4530
+ })
4531
+ }
4532
+ ];
4533
+ var BEHAVIOR_TEMPLATES = [
4534
+ // Timing burst
4535
+ {
4536
+ pattern: "timing-burst",
4537
+ category: "behavior",
4538
+ template: "I detected a burst of {txCount} transactions within {timeSpan}. This concentrated activity stands out and could correlate with real-world events or identify this wallet among others.",
4539
+ extractVariables: (signal) => {
4540
+ const txMatch = signal.reason?.match(/(\d+)\s+transaction/);
4541
+ const timeMatch = signal.reason?.match(/within\s+([^\.]+)/);
4542
+ return {
4543
+ txCount: txMatch?.[1] || signal.evidence.length || "?",
4544
+ timeSpan: timeMatch?.[1] || "a short period"
4545
+ };
4546
+ }
4547
+ },
4548
+ // Timing regular interval
4549
+ {
4550
+ pattern: "timing-regular-interval",
4551
+ category: "behavior",
4552
+ template: "I can see transactions happening at regular intervals. This clock-like precision tells me this is likely automated, revealing either a bot or a fixed personal schedule.",
4553
+ extractVariables: (signal) => {
4554
+ const match = signal.reason?.match(/(\d+)[- ]?(minute|hour|day)/i);
4555
+ return {
4556
+ interval: match ? `${match[1]} ${match[2]}` : "predictable"
4557
+ };
4558
+ }
4559
+ },
4560
+ // Timing timezone
4561
+ {
4562
+ pattern: "timing-timezone-pattern",
4563
+ category: "behavior",
4564
+ template: "I can determine that {concentration}% of transactions happen during specific hours ({activeHours} UTC). This reveals the user's timezone and daily schedule, narrowing down their geographic location.",
4565
+ extractVariables: (signal) => {
4566
+ const concMatch = signal.reason?.match(/(\d+)%/);
4567
+ const hoursMatch = signal.reason?.match(/during\s+(\d+:\d+.*?)\s*UTC/i);
4568
+ return {
4569
+ concentration: concMatch?.[1] || "40",
4570
+ activeHours: hoursMatch?.[1] || "consistent hours"
4571
+ };
4572
+ }
4573
+ },
4574
+ // Priority fee consistent
4575
+ {
4576
+ pattern: "priority-fee-consistent",
4577
+ category: "behavior",
4578
+ template: "I notice that many transactions use consistent priority fees. This pattern acts as a signature linking these transactions together.",
4579
+ extractVariables: (signal) => {
4580
+ const concMatch = signal.reason?.match(/(\d+)%/);
4581
+ const feeMatch = signal.reason?.match(/(\d+)\s*lamports/);
4582
+ return {
4583
+ concentration: concMatch?.[1] || "50",
4584
+ feeAmount: feeMatch?.[1] || "specific amounts"
4585
+ };
4586
+ }
4587
+ },
4588
+ // Compute budget fingerprint
4589
+ {
4590
+ pattern: "compute-budget-fingerprint",
4591
+ category: "behavior",
4592
+ template: "I can identify a distinctive compute unit pattern in {concentration}% of transactions. This consistency helps identify transactions from the same source.",
4593
+ extractVariables: (signal) => {
4594
+ const match = signal.reason?.match(/(\d+)%/);
4595
+ return {
4596
+ concentration: match?.[1] || "60"
4597
+ };
4598
+ }
4599
+ },
4600
+ // Instruction sequence pattern
4601
+ {
4602
+ pattern: "instruction-sequence-pattern",
4603
+ category: "behavior",
4604
+ template: "I can identify {sequenceCount} repeated instruction sequence(s). Even across different wallets, this operational fingerprint could link them together.",
4605
+ extractVariables: (signal) => ({
4606
+ sequenceCount: signal.evidence.length || 1
4607
+ })
4608
+ },
4609
+ // Program usage profile
4610
+ {
4611
+ pattern: "program-usage-profile",
4612
+ category: "behavior",
4613
+ template: "I have profiled this wallet's program usage - they regularly interact with {programCount} distinctive programs. This combination creates a behavioral fingerprint that could identify this user across wallets.",
4614
+ extractVariables: (signal) => ({
4615
+ programCount: signal.evidence.length
4616
+ })
4617
+ },
4618
+ // Program reuse
4619
+ {
4620
+ pattern: "program-reuse",
4621
+ category: "behavior",
4622
+ template: "I can see this wallet repeatedly uses the same {programCount} program(s). This pattern of protocol preferences helps identify the user's DeFi strategy and habits.",
4623
+ extractVariables: (signal) => ({
4624
+ programCount: signal.evidence.length
4625
+ })
4626
+ },
4627
+ // Instruction PDA reuse
4628
+ {
4629
+ pattern: "instruction-pda-reuse",
4630
+ category: "behavior",
4631
+ template: "I can track repeated interactions with {pdaCount} program-derived account(s) at the instruction level, creating a detailed activity trail.",
4632
+ extractVariables: (signal) => ({
4633
+ pdaCount: signal.evidence.length
4634
+ })
4635
+ },
4636
+ // Instruction type patterns (dynamic)
4637
+ {
4638
+ pattern: /^instruction-type-/,
4639
+ category: "behavior",
4640
+ template: "I see a specific operation type being used {count} times. This repetitive behavior pattern distinguishes this wallet from normal users.",
4641
+ extractVariables: (signal) => {
4642
+ const match = signal.reason?.match(/(\d+)\s+times/);
4643
+ return {
4644
+ count: match?.[1] || "many"
4645
+ };
4646
+ }
4647
+ },
4648
+ // Stake delegation pattern
4649
+ {
4650
+ pattern: "stake-delegation-pattern",
4651
+ category: "behavior",
4652
+ template: "I can see staking activity concentrated on {validatorCount} validator(s). This choice reveals preferences that could help identify the staker, especially if using uncommon validators.",
4653
+ extractVariables: (signal) => ({
4654
+ validatorCount: signal.evidence.length
4655
+ })
4656
+ },
4657
+ // Stake timing correlation
4658
+ {
4659
+ pattern: "stake-timing-correlation",
4660
+ category: "behavior",
4661
+ template: "I detect staking operations at regular intervals. This schedule reveals either automation or habitual behavior patterns.",
4662
+ extractVariables: (signal) => {
4663
+ const match = signal.reason?.match(/(\d+)[- ]?hour/);
4664
+ return {
4665
+ interval: match?.[1] || "regular"
4666
+ };
4667
+ }
4668
+ },
4669
+ // Address high diversity
4670
+ {
4671
+ pattern: "address-high-diversity",
4672
+ category: "behavior",
4673
+ template: "I can see this single wallet is used for {activityCount} different types of activity. This makes it easy to build a complete profile of the owner's interests and habits.",
4674
+ extractVariables: (signal) => {
4675
+ const match = signal.reason?.match(/(\d+)\s+different/);
4676
+ return {
4677
+ activityCount: match?.[1] || "4"
4678
+ };
4679
+ }
4680
+ },
4681
+ // Address moderate diversity
4682
+ {
4683
+ pattern: "address-moderate-diversity",
4684
+ category: "behavior",
4685
+ template: "I can see this wallet is used for multiple activity types. These activities are now connected to the same identity.",
4686
+ extractVariables: (signal) => {
4687
+ const match = signal.reason?.match(/(\d+)\s+activity/);
4688
+ return {
4689
+ activityCount: match?.[1] || "3"
4690
+ };
4691
+ }
4692
+ },
4693
+ // Address long-term usage
4694
+ {
4695
+ pattern: "address-long-term-usage",
4696
+ category: "behavior",
4697
+ template: "I have access to {dayCount} days of transaction history with {txCount} transactions on this wallet. This extended timeline provides a detailed view of the owner's behavior over time.",
4698
+ extractVariables: (signal) => {
4699
+ const dayMatch = signal.reason?.match(/(\d+)\s*days/);
4700
+ const txMatch = signal.reason?.match(/(\d+)\s*transaction/);
4701
+ return {
4702
+ dayCount: dayMatch?.[1] || "many",
4703
+ txCount: txMatch?.[1] || "many"
4704
+ };
4705
+ }
4706
+ }
4707
+ ];
4708
+ var EXPOSURE_TEMPLATES = [
4709
+ // Memo PII exposure (critical)
4710
+ {
4711
+ pattern: "memo-pii-exposure",
4712
+ category: "exposure",
4713
+ template: "I found personal information directly embedded in {memoCount} transaction memo(s). This data - including possible email addresses, phone numbers, or names - is permanently visible on the blockchain and directly links this wallet to a real identity.",
4714
+ detailTemplates: ['Memo content found: "{content}"'],
4715
+ extractVariables: (signal) => ({
4716
+ memoCount: signal.evidence.length
4717
+ })
4718
+ },
4719
+ // Memo descriptive content
4720
+ {
4721
+ pattern: "memo-descriptive-content",
4722
+ category: "exposure",
4723
+ template: "I can read {memoCount} descriptive memo(s) containing URLs, payment references, or descriptions. This context reveals the purpose of transactions and provides additional identifying information.",
4724
+ extractVariables: (signal) => ({
4725
+ memoCount: signal.evidence.length
4726
+ })
4727
+ },
4728
+ // Memo usage (low severity)
4729
+ {
4730
+ pattern: "memo-usage",
4731
+ category: "exposure",
4732
+ template: "I see that {txCount} transaction(s) include memo data. While the current content may seem harmless, any memo adds extra context that observers can use to understand this wallet's activity.",
4733
+ extractVariables: (signal) => {
4734
+ const match = signal.reason?.match(/(\d+)\s+transaction/);
4735
+ return {
4736
+ txCount: match?.[1] || signal.evidence.length || "?"
4737
+ };
4738
+ }
4739
+ },
4740
+ // Token account churn
4741
+ {
4742
+ pattern: "token-account-churn",
4743
+ category: "exposure",
4744
+ template: 'I can trace {closeCount} closed token account(s) through their rent refunds sent back to the owner. This reveals which wallets own the "throwaway" accounts.',
4745
+ extractVariables: (signal) => {
4746
+ const match = signal.reason?.match(/(\d+)/);
4747
+ return {
4748
+ closeCount: match?.[1] || signal.evidence.length || "?"
4749
+ };
4750
+ }
4751
+ },
4752
+ // Token account short-lived
4753
+ {
4754
+ pattern: "token-account-short-lived",
4755
+ category: "exposure",
4756
+ template: "I detected {accountCount} token account(s) that were created and closed within an hour. This throwaway pattern still leaves a trail through the rent refunds.",
4757
+ extractVariables: (signal) => ({
4758
+ accountCount: signal.evidence.length
4759
+ })
4760
+ },
4761
+ // Token account common owner
4762
+ {
4763
+ pattern: "token-account-common-owner",
4764
+ category: "exposure",
4765
+ template: "I can see that {ownerCount} wallet(s) own multiple token accounts. Since ownership is public, I can link all these accounts to their common owners.",
4766
+ extractVariables: (signal) => ({
4767
+ ownerCount: signal.evidence.length
4768
+ })
4769
+ },
4770
+ // Rent refund clustering
4771
+ {
4772
+ pattern: "rent-refund-clustering",
4773
+ category: "exposure",
4774
+ template: "I traced rent refunds from multiple closed accounts back to the same address(es). This exposes the full scope of their token holdings.",
4775
+ extractVariables: (signal) => ({
4776
+ recipientCount: signal.evidence.length
4777
+ })
4778
+ },
4779
+ // Known entity other
4780
+ {
4781
+ pattern: "known-entity-other",
4782
+ category: "exposure",
4783
+ template: "I can see interactions with {entityCount} known service(s). These public landmarks in the transaction history reveal which protocols and services this wallet uses.",
4784
+ extractVariables: (signal) => ({
4785
+ entityCount: signal.evidence.length
4786
+ })
4787
+ },
4788
+ // Counterparty-program combo
4789
+ {
4790
+ pattern: "counterparty-program-combo",
4791
+ category: "exposure",
4792
+ template: "I identified {comboCount} distinctive counterparty-program combination(s) used repeatedly. This specific pairing is highly identifying - much more so than either element alone.",
4793
+ extractVariables: (signal) => ({
4794
+ comboCount: signal.evidence.length
4795
+ })
4796
+ }
4797
+ ];
4798
+ var ALL_TEMPLATES = [
4799
+ ...IDENTITY_TEMPLATES,
4800
+ ...CONNECTIONS_TEMPLATES,
4801
+ ...BEHAVIOR_TEMPLATES,
4802
+ ...EXPOSURE_TEMPLATES
4803
+ ];
4804
+ function findTemplate(signalId) {
4805
+ for (const template of ALL_TEMPLATES) {
4806
+ if (typeof template.pattern === "string") {
4807
+ if (template.pattern === signalId) return template;
4808
+ } else {
4809
+ if (template.pattern.test(signalId)) return template;
4810
+ }
4811
+ }
4812
+ return void 0;
4813
+ }
4814
+
4815
+ // src/narrative/categories.ts
4816
+ var CATEGORY_DEFINITIONS = {
4817
+ identity: {
4818
+ id: "identity",
4819
+ title: "Identity Linkage",
4820
+ description: "Direct connections to real-world identity",
4821
+ openingPhrases: [
4822
+ "Starting with the most critical findings - I can potentially identify who owns this wallet.",
4823
+ "The most serious privacy concerns involve direct identity linkage.",
4824
+ "I have found clear paths to identifying the wallet owner."
4825
+ ],
4826
+ closingPhrases: [
4827
+ "These identity links are the most damaging because they connect on-chain activity to real-world identities.",
4828
+ "Any of these pathways could lead to complete identification of the wallet owner."
4829
+ ],
4830
+ priority: 1
4831
+ },
4832
+ connections: {
4833
+ id: "connections",
4834
+ title: "Wallet Connections",
4835
+ description: "Links between this wallet and other addresses",
4836
+ openingPhrases: [
4837
+ "I can map out a network of connected wallets.",
4838
+ "This wallet is linked to other addresses in several ways.",
4839
+ "The following connections reveal relationships between wallets."
4840
+ ],
4841
+ closingPhrases: [
4842
+ "These connections form a web - identifying any one wallet helps identify the others.",
4843
+ "Even without knowing the owner, I can see which wallets belong together."
4844
+ ],
4845
+ priority: 2
4846
+ },
4847
+ behavior: {
4848
+ id: "behavior",
4849
+ title: "Behavioral Fingerprints",
4850
+ description: "Patterns that distinguish this wallet from others",
4851
+ openingPhrases: [
4852
+ "I can identify distinctive behavioral patterns.",
4853
+ "This wallet has recognizable usage patterns.",
4854
+ "The following behaviors create a unique fingerprint."
4855
+ ],
4856
+ closingPhrases: [
4857
+ "These patterns help identify the same user across different wallets.",
4858
+ "Behavioral consistency makes this wallet stand out from random users."
4859
+ ],
4860
+ priority: 3
4861
+ },
4862
+ exposure: {
4863
+ id: "exposure",
4864
+ title: "Information Exposure",
4865
+ description: "Metadata and content that reveals information",
4866
+ openingPhrases: [
4867
+ "Additional information is exposed through transaction data.",
4868
+ "I found metadata that provides extra context about this wallet.",
4869
+ "The following data leaks provide additional insights."
4870
+ ],
4871
+ closingPhrases: [
4872
+ "This exposed information adds context that helps in identification.",
4873
+ "Each piece of exposed data contributes to a more complete profile."
4874
+ ],
4875
+ priority: 4
4876
+ }
4877
+ };
4878
+ var SIGNAL_CATEGORY_MAP = {
4879
+ // Identity - direct paths to real-world identity
4880
+ "known-entity-exchange": "identity",
4881
+ "known-entity-bridge": "identity",
4882
+ "domain-name-linkage": "identity",
4883
+ "nft-metadata-exposure": "identity",
4884
+ // Connections - wallet relationships
4885
+ "fee-payer-external": "connections",
4886
+ "fee-payer-never-self": "connections",
4887
+ "fee-payer-multi-signer": "connections",
4888
+ "signer-repeated": "connections",
4889
+ "signer-set-reuse": "connections",
4890
+ "signer-authority-hub": "connections",
4891
+ "ata-creator-linkage": "connections",
4892
+ "ata-funding-pattern": "connections",
4893
+ "counterparty-reuse": "connections",
4894
+ "pda-reuse": "connections",
4895
+ // Behavior - usage patterns
4896
+ "timing-burst": "behavior",
4897
+ "timing-regular-interval": "behavior",
4898
+ "timing-timezone-pattern": "behavior",
4899
+ "priority-fee-consistent": "behavior",
4900
+ "compute-budget-fingerprint": "behavior",
4901
+ "instruction-sequence-pattern": "behavior",
4902
+ "program-usage-profile": "behavior",
4903
+ "program-reuse": "behavior",
4904
+ "instruction-pda-reuse": "behavior",
4905
+ "stake-delegation-pattern": "behavior",
4906
+ "stake-timing-correlation": "behavior",
4907
+ "address-high-diversity": "behavior",
4908
+ "address-moderate-diversity": "behavior",
4909
+ "address-long-term-usage": "behavior",
4910
+ // Exposure - information leaks
4911
+ "memo-usage": "exposure",
4912
+ "memo-pii-exposure": "exposure",
4913
+ "memo-descriptive-content": "exposure",
4914
+ "known-entity-other": "exposure",
4915
+ "token-account-churn": "exposure",
4916
+ "token-account-short-lived": "exposure",
4917
+ "token-account-common-owner": "exposure",
4918
+ "rent-refund-clustering": "exposure",
4919
+ "counterparty-program-combo": "exposure"
4920
+ };
4921
+ function getSignalCategory(signalId) {
4922
+ if (SIGNAL_CATEGORY_MAP[signalId]) {
4923
+ return SIGNAL_CATEGORY_MAP[signalId];
4924
+ }
4925
+ if (signalId.startsWith("known-entity-frequent-")) return "identity";
4926
+ if (signalId.startsWith("instruction-type-")) return "behavior";
4927
+ return "exposure";
4928
+ }
4929
+ function getCategoriesInOrder() {
4930
+ return ["identity", "connections", "behavior", "exposure"];
4931
+ }
4932
+
4933
+ // src/narrative/transitions.ts
4934
+ var INTRA_CATEGORY_TRANSITIONS = {
4935
+ additive: [
4936
+ "Additionally,",
4937
+ "Furthermore,",
4938
+ "I also found that",
4939
+ "Building on this,",
4940
+ "On top of that,",
4941
+ "Moreover,"
4942
+ ],
4943
+ amplifying: [
4944
+ "More concerning,",
4945
+ "Even more revealing,",
4946
+ "This is compounded by the fact that",
4947
+ "What makes this worse is that",
4948
+ "Adding to this risk,"
4949
+ ],
4950
+ neutral: ["I can also see that", "Another observation:", "Looking further,", "In addition,"]
4951
+ };
4952
+ function selectTransition(currentSeverity, previousSeverity, index) {
4953
+ const severityRank = { LOW: 1, MEDIUM: 2, HIGH: 3 };
4954
+ if (index === 0) return "";
4955
+ if (severityRank[currentSeverity] > severityRank[previousSeverity]) {
4956
+ return INTRA_CATEGORY_TRANSITIONS.amplifying[index % INTRA_CATEGORY_TRANSITIONS.amplifying.length];
4957
+ }
4958
+ const transitions = index % 2 === 0 ? INTRA_CATEGORY_TRANSITIONS.additive : INTRA_CATEGORY_TRANSITIONS.neutral;
4959
+ return transitions[index % transitions.length];
4960
+ }
4961
+ function getSeverityIndicator(severity) {
4962
+ switch (severity) {
4963
+ case "HIGH":
4964
+ return "[!]";
4965
+ case "MEDIUM":
4966
+ return "[~]";
4967
+ case "LOW":
4968
+ return "[.]";
4969
+ }
4970
+ }
4971
+
4972
+ // src/narrative/conclusion.ts
4973
+ function determineIdentifiability(report) {
4974
+ const { signals, overallRisk, summary } = report;
4975
+ const hasExchangeInteraction = signals.some((s) => s.id === "known-entity-exchange");
4976
+ const hasDomainLinkage = signals.some((s) => s.id === "domain-name-linkage");
4977
+ const hasPIIInMemos = signals.some((s) => s.id === "memo-pii-exposure");
4978
+ const hasNeverSelfPays = signals.some((s) => s.id === "fee-payer-never-self");
4979
+ if (hasPIIInMemos && hasExchangeInteraction) {
4980
+ return "fully-identified";
4981
+ }
4982
+ if (hasDomainLinkage && hasExchangeInteraction) {
4983
+ return "fully-identified";
4984
+ }
4985
+ if (hasExchangeInteraction || hasDomainLinkage || hasPIIInMemos) {
4986
+ return "identifiable";
4987
+ }
4988
+ const hasStrongLinkage = signals.some(
4989
+ (s) => s.id === "fee-payer-never-self" || s.id === "signer-authority-hub" || s.id === "ata-creator-linkage"
4990
+ );
4991
+ if (hasStrongLinkage && summary.highRiskSignals >= 2) {
4992
+ return "identifiable";
4993
+ }
4994
+ if (overallRisk === "HIGH" || overallRisk === "MEDIUM") {
4995
+ return "pseudonymous";
4996
+ }
4997
+ if (summary.totalSignals > 0) {
4998
+ return "pseudonymous";
4999
+ }
5000
+ return "anonymous";
5001
+ }
5002
+ function generateConclusion(report, statements, identifiability) {
5003
+ const categoryCount = new Set(statements.map((s) => s.category)).size;
5004
+ const highCount = statements.filter((s) => s.severity === "HIGH").length;
5005
+ const conclusions = {
5006
+ "fully-identified": [
5007
+ `In summary, I can confidently identify the owner of this wallet. With direct exchange interactions combined with personal information exposure, the on-chain pseudonym is broken. The ${highCount} critical issues detected provide multiple pathways to real-world identification.`,
5008
+ `This wallet is fully identified. Through ${categoryCount} categories of privacy leaks, I have established clear links between this on-chain address and real-world identity. The privacy damage is complete and permanent.`
5009
+ ],
5010
+ identifiable: [
5011
+ `This wallet is identifiable through ${highCount} high-severity privacy issues. While I may not have a name yet, there are clear pathways - likely through exchange records or domain registration - that could reveal the owner's identity with minimal additional investigation.`,
5012
+ `I can likely identify this wallet's owner. The patterns across ${categoryCount} categories create enough data points that, combined with external records, would reveal their identity. The wallet operates more like a named account than an anonymous one.`
5013
+ ],
5014
+ pseudonymous: [
5015
+ `This wallet maintains pseudonymity but has revealing patterns. Across ${categoryCount} categories, I found behavioral fingerprints and connections that distinguish this user from others. While not immediately identifiable, these patterns could be used to link this wallet to other activities or narrow down the owner's identity.`,
5016
+ `The wallet is pseudonymous - not anonymous. I cannot immediately identify the owner, but the ${statements.length} patterns detected create a profile that could be matched against other data sources or used to connect this wallet to the same owner's other activities.`
5017
+ ],
5018
+ anonymous: [
5019
+ "This wallet maintains reasonable anonymity. While some minor patterns exist, there are no critical privacy issues that would allow identification. The owner practices good privacy hygiene.",
5020
+ "I found no significant privacy leaks. This wallet blends in with normal activity and would be difficult to distinguish or identify from on-chain data alone."
5021
+ ]
5022
+ };
5023
+ const options = conclusions[identifiability];
5024
+ return options[report.summary.totalSignals % options.length];
5025
+ }
5026
+ function getIdentifiabilityDescription(level) {
5027
+ const descriptions = {
5028
+ "fully-identified": "Direct pathways to real-world identity exist through KYC exchanges and personal information exposure.",
5029
+ identifiable: "Strong linkage to identity through exchanges, domains, or exposed personal information.",
5030
+ pseudonymous: "Behavioral patterns exist that distinguish this wallet, but no direct identity link.",
5031
+ anonymous: "Minimal distinguishing characteristics; wallet blends in with normal activity."
5032
+ };
5033
+ return descriptions[level];
5034
+ }
5035
+
5036
+ // src/narrative/builder.ts
5037
+ var DEFAULT_OPTIONS = {
5038
+ includeLowSeverity: true,
5039
+ includeDetails: true,
5040
+ maxStatementsPerCategory: 5,
5041
+ format: "text"
5042
+ };
5043
+ function buildStatement(signal) {
5044
+ const template = findTemplate(signal.id);
5045
+ if (!template) {
5046
+ return {
5047
+ signalId: signal.id,
5048
+ category: getSignalCategory(signal.id),
5049
+ severity: signal.severity,
5050
+ statement: `I can determine that: ${signal.reason}`,
5051
+ details: signal.evidence.slice(0, 3).map((e) => e.description),
5052
+ confidence: signal.confidence ?? 0.7
5053
+ };
5054
+ }
5055
+ const variables = template.extractVariables(signal);
5056
+ const statement = interpolate(template.template, variables);
5057
+ const details = [];
5058
+ if (template.detailTemplates && signal.evidence.length > 0) {
5059
+ for (let i = 0; i < Math.min(signal.evidence.length, 3); i++) {
5060
+ const ev = signal.evidence[i];
5061
+ const detailVars = {
5062
+ ...variables,
5063
+ content: ev.description?.replace(/^"(.+)"$/, "$1") || "",
5064
+ reference: ev.reference || ""
5065
+ };
5066
+ const detail = interpolate(template.detailTemplates[0], detailVars);
5067
+ details.push(detail);
5068
+ }
5069
+ }
5070
+ return {
5071
+ signalId: signal.id,
5072
+ category: template.category,
5073
+ severity: signal.severity,
5074
+ statement,
5075
+ details,
5076
+ confidence: signal.confidence ?? 0.7
5077
+ };
5078
+ }
5079
+ function groupByCategory(statements) {
5080
+ const groups = /* @__PURE__ */ new Map();
5081
+ for (const stmt of statements) {
5082
+ if (!groups.has(stmt.category)) {
5083
+ groups.set(stmt.category, []);
5084
+ }
5085
+ groups.get(stmt.category).push(stmt);
5086
+ }
5087
+ for (const [, stmts] of groups) {
5088
+ stmts.sort((a, b) => {
5089
+ const order = { HIGH: 0, MEDIUM: 1, LOW: 2 };
5090
+ return order[a.severity] - order[b.severity];
5091
+ });
5092
+ }
5093
+ return groups;
5094
+ }
5095
+ function buildParagraph(category, statements, options) {
5096
+ const def = CATEGORY_DEFINITIONS[category];
5097
+ const limited = statements.slice(0, options.maxStatementsPerCategory);
5098
+ const hasHigh = limited.some((s) => s.severity === "HIGH");
5099
+ const openingIndex = hasHigh ? 0 : 1;
5100
+ return {
5101
+ category,
5102
+ title: def.title,
5103
+ opening: def.openingPhrases[openingIndex % def.openingPhrases.length],
5104
+ statements: limited,
5105
+ closing: def.closingPhrases[0]
5106
+ };
5107
+ }
5108
+ function generateIntroduction(report) {
5109
+ const { overallRisk, summary } = report;
5110
+ if (overallRisk === "HIGH") {
5111
+ return `Based on my analysis of ${summary.transactionsAnalyzed} transactions, I can build a detailed profile of this wallet's owner. There are ${summary.highRiskSignals} critical privacy issues that could lead to identification.`;
5112
+ }
5113
+ if (overallRisk === "MEDIUM") {
5114
+ return `After analyzing ${summary.transactionsAnalyzed} transactions, I found several privacy patterns that reveal information about this wallet's owner. While not immediately identifiable, there are ${summary.totalSignals} concerns worth addressing.`;
5115
+ }
5116
+ return `My analysis of ${summary.transactionsAnalyzed} transactions reveals ${summary.totalSignals} minor privacy patterns. This wallet maintains reasonable privacy, though some behavioral fingerprints exist.`;
5117
+ }
5118
+ function formatAsText(narrative, options) {
5119
+ const lines = [];
5120
+ lines.push("");
5121
+ lines.push("=".repeat(65));
5122
+ lines.push(" WHAT DOES THE OBSERVER KNOW?");
5123
+ lines.push(" An Adversary's Perspective");
5124
+ lines.push("=".repeat(65));
5125
+ lines.push("");
5126
+ lines.push(narrative.introduction);
5127
+ lines.push("");
5128
+ for (let i = 0; i < narrative.paragraphs.length; i++) {
5129
+ const para = narrative.paragraphs[i];
5130
+ lines.push("-".repeat(65));
5131
+ lines.push(`## ${para.title.toUpperCase()}`);
5132
+ lines.push("-".repeat(65));
5133
+ lines.push("");
5134
+ lines.push(para.opening);
5135
+ lines.push("");
5136
+ for (let j = 0; j < para.statements.length; j++) {
5137
+ const stmt = para.statements[j];
5138
+ const prevSeverity = j > 0 ? para.statements[j - 1].severity : stmt.severity;
5139
+ const transition = selectTransition(stmt.severity, prevSeverity, j);
5140
+ const severityIcon = getSeverityIndicator(stmt.severity);
5141
+ if (transition) {
5142
+ lines.push(`${transition} ${severityIcon} ${stmt.statement}`);
5143
+ } else {
5144
+ lines.push(`${severityIcon} ${stmt.statement}`);
5145
+ }
5146
+ if (options.includeDetails && stmt.details.length > 0) {
5147
+ for (const detail of stmt.details) {
5148
+ lines.push(` - ${detail}`);
5149
+ }
5150
+ }
5151
+ lines.push("");
5152
+ }
5153
+ lines.push(para.closing);
5154
+ lines.push("");
5155
+ }
5156
+ lines.push("=".repeat(65));
5157
+ lines.push("## CONCLUSION");
5158
+ lines.push("=".repeat(65));
5159
+ lines.push("");
5160
+ lines.push(narrative.conclusion);
5161
+ lines.push("");
5162
+ lines.push(`Identifiability Level: ${narrative.identifiabilityLevel.toUpperCase()}`);
5163
+ lines.push("");
5164
+ return lines.join("\n");
5165
+ }
5166
+ function generateNarrative(report, options = {}) {
5167
+ const opts = { ...DEFAULT_OPTIONS, ...options };
5168
+ let signals = report.signals;
5169
+ if (!opts.includeLowSeverity) {
5170
+ signals = signals.filter((s) => s.severity !== "LOW");
5171
+ }
5172
+ const statements = [];
5173
+ for (const signal of signals) {
5174
+ const stmt = buildStatement(signal);
5175
+ if (stmt) {
5176
+ statements.push(stmt);
5177
+ }
5178
+ }
5179
+ const grouped = groupByCategory(statements);
5180
+ const categoryOrder = getCategoriesInOrder();
5181
+ const paragraphs = [];
5182
+ for (const category of categoryOrder) {
5183
+ const categoryStatements = grouped.get(category);
5184
+ if (categoryStatements && categoryStatements.length > 0) {
5185
+ paragraphs.push(buildParagraph(category, categoryStatements, opts));
5186
+ }
5187
+ }
5188
+ const identifiability = determineIdentifiability(report);
5189
+ const conclusion = generateConclusion(report, statements, identifiability);
5190
+ return {
5191
+ introduction: generateIntroduction(report),
5192
+ paragraphs,
5193
+ conclusion,
5194
+ identifiabilityLevel: identifiability,
5195
+ signalCount: signals.length,
5196
+ timestamp: Date.now()
5197
+ };
5198
+ }
5199
+ function generateNarrativeText(report, options = {}) {
5200
+ const opts = { ...DEFAULT_OPTIONS, ...options };
5201
+ const narrative = generateNarrative(report, opts);
5202
+ return formatAsText(narrative, opts);
5203
+ }
3666
5204
  // Annotate the CommonJS export names for ESM import in node:
3667
5205
  0 && (module.exports = {
5206
+ ALL_TEMPLATES,
5207
+ CATEGORY_DEFINITIONS,
3668
5208
  DEFAULT_CONFIG,
5209
+ FileNicknameProvider,
5210
+ MemoryNicknameProvider,
3669
5211
  PERMISSIVE_CONFIG,
3670
5212
  PrivacyConfigSchema,
3671
5213
  RPCClient,
@@ -3678,7 +5220,10 @@ function getTestWallet(config) {
3678
5220
  collectTransactionData,
3679
5221
  collectWalletData,
3680
5222
  compareImplementations,
5223
+ createAddressFormatter,
3681
5224
  createDefaultLabelProvider,
5225
+ createFileNicknameProvider,
5226
+ createMemoryNicknameProvider,
3682
5227
  detectATALinkage,
3683
5228
  detectAddressReuse,
3684
5229
  detectCounterpartyReuse,
@@ -3687,6 +5232,7 @@ function getTestWallet(config) {
3687
5232
  detectIdentityMetadataExposure,
3688
5233
  detectInstructionFingerprinting,
3689
5234
  detectKnownEntityInteraction,
5235
+ detectLocationInference,
3690
5236
  detectMemoExposure,
3691
5237
  detectMemoPII,
3692
5238
  detectPriorityFeeFingerprinting,
@@ -3694,19 +5240,33 @@ function getTestWallet(config) {
3694
5240
  detectStakingDelegationPatterns,
3695
5241
  detectTimingPatterns,
3696
5242
  detectTokenAccountLifecycle,
5243
+ determineIdentifiability,
5244
+ displayAddress,
5245
+ displayAddresses,
3697
5246
  evaluateHeuristics,
5247
+ findTemplate,
5248
+ generateConclusion,
5249
+ generateNarrative,
5250
+ generateNarrativeText,
3698
5251
  generateReport,
5252
+ getAddressDisplayInfo,
5253
+ getCategoriesInOrder,
3699
5254
  getEnvironmentConfig,
5255
+ getIdentifiabilityDescription,
3700
5256
  getMemoRecommendations,
5257
+ getSignalCategory,
3701
5258
  getTestWallet,
3702
5259
  groupIssuesByFile,
3703
5260
  loadConfig,
3704
5261
  normalizeProgramData,
3705
5262
  normalizeTransactionData,
3706
5263
  normalizeWalletData,
5264
+ parseNicknameStore,
5265
+ serializeNicknameStore,
3707
5266
  shouldEnforce,
3708
5267
  simulateTransactionFlow,
3709
5268
  simulateTransactionPrivacy,
3710
- sortIssues
5269
+ sortIssues,
5270
+ truncateAddress
3711
5271
  });
3712
5272
  //# sourceMappingURL=index.cjs.map