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 +1628 -68
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +1605 -67
- package/dist/index.js.map +4 -4
- package/dist/known-addresses.json +173 -9
- package/dist/narrative.cjs +912 -0
- package/dist/narrative.cjs.map +7 -0
- package/dist/narrative.js +876 -0
- package/dist/narrative.js.map +7 -0
- package/dist/normalizer.cjs +717 -0
- package/dist/normalizer.cjs.map +7 -0
- package/dist/normalizer.js +688 -0
- package/dist/normalizer.js.map +7 -0
- package/dist/scanner.cjs +1933 -0
- package/dist/scanner.cjs.map +7 -0
- package/dist/scanner.js +1905 -0
- package/dist/scanner.js.map +7 -0
- package/package.json +16 -1
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 = "
|
|
105
|
+
var _RPC_ENCODED = "aHR0cHM6Ly9sYXRlLWhhcmR3b3JraW5nLXdhdGVyZmFsbC5zb2xhbmEtbWFpbm5ldC5xdWlrbm9kZS5wcm8vNDAxN2I0OGFjZjNhMmExNjY1NjAzY2FjMDk2ODIyY2U0YmVjM2E5MC8=";
|
|
84
106
|
var DEFAULT_RPC_URL = /* @__PURE__ */ Buffer.from(_RPC_ENCODED, "base64").toString("utf-8");
|
|
85
|
-
var VERSION = "0.
|
|
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
|
-
|
|
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
|
|
479
|
+
blockTime: tx?.blockTime ?? null,
|
|
441
480
|
feePayer: "unknown",
|
|
442
481
|
signers: []
|
|
443
482
|
};
|
|
444
483
|
}
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
|
463
|
-
if (
|
|
464
|
-
for (const instruction of
|
|
465
|
-
if (!instruction
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
memo
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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,
|
|
4277
|
+
if ((0, import_fs3.existsSync)(fullPath)) {
|
|
3604
4278
|
try {
|
|
3605
|
-
const content = (0,
|
|
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,
|
|
4290
|
+
if ((0, import_fs3.existsSync)(packageJsonPath)) {
|
|
3617
4291
|
try {
|
|
3618
|
-
const content = (0,
|
|
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
|