hd-wallet-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/package.json +59 -0
- package/src/address-derivation.js +476 -0
- package/src/app.js +3302 -0
- package/src/blockchain-trust.js +699 -0
- package/src/constants.js +87 -0
- package/src/lib.js +63 -0
- package/src/template.js +348 -0
- package/src/trust-ui.js +745 -0
- package/src/wallet-storage.js +696 -0
- package/styles/main.css +4521 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blockchain Trust Transactions
|
|
3
|
+
*
|
|
4
|
+
* KeySpace-inspired trust model: all trust relationships are published
|
|
5
|
+
* as on-chain transactions using OP_RETURN (Bitcoin), memo fields (Solana),
|
|
6
|
+
* or transaction data (Ethereum).
|
|
7
|
+
*
|
|
8
|
+
* Binary encoding format (v2):
|
|
9
|
+
* Trust: [0x54][0x01][level][timestamp:4][pubkey:32-33] = 40-41 bytes
|
|
10
|
+
* Revocation: [0x52][0x01][timestamp:4][txhash:32] = 38 bytes
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Trust Levels (PGP-style)
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export const TrustLevel = {
|
|
18
|
+
NEVER: 1, // Blocklist / Do not trust
|
|
19
|
+
UNKNOWN: 2, // Default / No opinion
|
|
20
|
+
MARGINAL: 3, // Some trust
|
|
21
|
+
FULL: 4, // Full trust / Can sign other keys
|
|
22
|
+
ULTIMATE: 5, // Own keys
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const TrustLevelNames = {
|
|
26
|
+
1: 'Never',
|
|
27
|
+
2: 'Unknown',
|
|
28
|
+
3: 'Marginal',
|
|
29
|
+
4: 'Full',
|
|
30
|
+
5: 'Ultimate',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Binary Constants
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
const MAGIC_TRUST = 0x54; // 'T'
|
|
38
|
+
const MAGIC_REVOKE = 0x52; // 'R'
|
|
39
|
+
const VERSION = 0x01;
|
|
40
|
+
|
|
41
|
+
// Legacy ASCII prefixes
|
|
42
|
+
const LEGACY_TRUST_PREFIX = 'TRUST';
|
|
43
|
+
const LEGACY_REVOKE_PREFIX = 'REVOKE';
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Binary Encoding Helpers
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
function writeUint32(buf, offset, value) {
|
|
50
|
+
buf[offset] = (value >>> 24) & 0xff;
|
|
51
|
+
buf[offset + 1] = (value >>> 16) & 0xff;
|
|
52
|
+
buf[offset + 2] = (value >>> 8) & 0xff;
|
|
53
|
+
buf[offset + 3] = value & 0xff;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readUint32(buf, offset) {
|
|
57
|
+
return (
|
|
58
|
+
((buf[offset] << 24) >>> 0) +
|
|
59
|
+
(buf[offset + 1] << 16) +
|
|
60
|
+
(buf[offset + 2] << 8) +
|
|
61
|
+
buf[offset + 3]
|
|
62
|
+
) >>> 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hexToBytes(hex) {
|
|
66
|
+
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
67
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
68
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
69
|
+
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
70
|
+
}
|
|
71
|
+
return bytes;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function bytesToHex(bytes) {
|
|
75
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function bytesToBase64(bytes) {
|
|
79
|
+
if (typeof btoa === 'function') {
|
|
80
|
+
return btoa(String.fromCharCode(...bytes));
|
|
81
|
+
}
|
|
82
|
+
return Buffer.from(bytes).toString('base64');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function base64ToBytes(b64) {
|
|
86
|
+
if (typeof atob === 'function') {
|
|
87
|
+
const bin = atob(b64);
|
|
88
|
+
const bytes = new Uint8Array(bin.length);
|
|
89
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
90
|
+
return bytes;
|
|
91
|
+
}
|
|
92
|
+
return new Uint8Array(Buffer.from(b64, 'base64'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// Binary Trust Encoding (v2)
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Encode trust metadata as compact binary Uint8Array.
|
|
101
|
+
*
|
|
102
|
+
* Format:
|
|
103
|
+
* Byte [0]: Magic 0x54 ('T')
|
|
104
|
+
* Byte [1]: Version 0x01
|
|
105
|
+
* Byte [2]: Trust level (0x01-0x05)
|
|
106
|
+
* Bytes [3-6]: Timestamp as uint32 (seconds since epoch)
|
|
107
|
+
* Bytes [7-N]: Recipient pubkey bytes (33 for secp256k1, 32 for ed25519)
|
|
108
|
+
*
|
|
109
|
+
* @param {number} level - Trust level (1-5)
|
|
110
|
+
* @param {string} recipientPubkey - Hex-encoded public key
|
|
111
|
+
* @param {number} [timestamp] - Unix timestamp in milliseconds (default: now)
|
|
112
|
+
* @returns {Uint8Array} Binary encoded trust metadata
|
|
113
|
+
*/
|
|
114
|
+
export function encodeTrustMetadata(level, recipientPubkey, timestamp = Date.now()) {
|
|
115
|
+
if (level < 1 || level > 5) {
|
|
116
|
+
throw new Error(`Invalid trust level: ${level}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const pubkeyBytes = hexToBytes(recipientPubkey);
|
|
120
|
+
const timeSec = Math.floor(timestamp / 1000);
|
|
121
|
+
const buf = new Uint8Array(7 + pubkeyBytes.length);
|
|
122
|
+
|
|
123
|
+
buf[0] = MAGIC_TRUST;
|
|
124
|
+
buf[1] = VERSION;
|
|
125
|
+
buf[2] = level;
|
|
126
|
+
writeUint32(buf, 3, timeSec);
|
|
127
|
+
buf.set(pubkeyBytes, 7);
|
|
128
|
+
|
|
129
|
+
return buf;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Encode revocation metadata as compact binary Uint8Array.
|
|
134
|
+
*
|
|
135
|
+
* Format:
|
|
136
|
+
* Byte [0]: Magic 0x52 ('R')
|
|
137
|
+
* Byte [1]: Version 0x01
|
|
138
|
+
* Bytes [2-5]: Timestamp as uint32 (seconds since epoch)
|
|
139
|
+
* Bytes [6-37]: Original tx hash (32 bytes)
|
|
140
|
+
*
|
|
141
|
+
* @param {string} originalTxHash - Hex-encoded transaction hash
|
|
142
|
+
* @param {number} [timestamp] - Unix timestamp in milliseconds (default: now)
|
|
143
|
+
* @returns {Uint8Array} Binary encoded revocation metadata
|
|
144
|
+
*/
|
|
145
|
+
export function encodeRevocationMetadata(originalTxHash, timestamp = Date.now()) {
|
|
146
|
+
const hashBytes = hexToBytes(originalTxHash);
|
|
147
|
+
if (hashBytes.length !== 32) {
|
|
148
|
+
throw new Error(`Expected 32-byte tx hash, got ${hashBytes.length}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const timeSec = Math.floor(timestamp / 1000);
|
|
152
|
+
const buf = new Uint8Array(38);
|
|
153
|
+
|
|
154
|
+
buf[0] = MAGIC_REVOKE;
|
|
155
|
+
buf[1] = VERSION;
|
|
156
|
+
writeUint32(buf, 2, timeSec);
|
|
157
|
+
buf.set(hashBytes, 6);
|
|
158
|
+
|
|
159
|
+
return buf;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Legacy ASCII encoder for backwards compatibility.
|
|
164
|
+
* Format: TRUST:<version>:<level>:<timestamp>:<recipientPubkey>
|
|
165
|
+
*/
|
|
166
|
+
export function encodeTrustMetadataLegacy(level, recipientPubkey, timestamp = Date.now()) {
|
|
167
|
+
if (level < 1 || level > 5) {
|
|
168
|
+
throw new Error(`Invalid trust level: ${level}`);
|
|
169
|
+
}
|
|
170
|
+
return `${LEGACY_TRUST_PREFIX}:1:${level}:${timestamp}:${recipientPubkey}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// Parsing (binary + legacy ASCII)
|
|
175
|
+
// =============================================================================
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Parse trust metadata from either binary (Uint8Array) or legacy ASCII string.
|
|
179
|
+
* Detects format by checking the first byte: 0x54 = binary trust, 0x52 = binary revoke.
|
|
180
|
+
*
|
|
181
|
+
* @param {Uint8Array|string} metadata - Binary buffer or ASCII string
|
|
182
|
+
* @returns {object|null} Parsed trust/revocation object, or null
|
|
183
|
+
*/
|
|
184
|
+
export function parseTrustMetadata(metadata) {
|
|
185
|
+
// Binary path
|
|
186
|
+
if (metadata instanceof Uint8Array || metadata instanceof ArrayBuffer) {
|
|
187
|
+
const buf = metadata instanceof ArrayBuffer ? new Uint8Array(metadata) : metadata;
|
|
188
|
+
return parseBinaryMetadata(buf);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// If it's a string, check if the first char signals binary
|
|
192
|
+
if (typeof metadata === 'string') {
|
|
193
|
+
// Could be base64-encoded binary; try legacy ASCII first
|
|
194
|
+
const legacy = parseLegacyMetadata(metadata);
|
|
195
|
+
if (legacy) return legacy;
|
|
196
|
+
|
|
197
|
+
// Try base64 decode
|
|
198
|
+
try {
|
|
199
|
+
const bytes = base64ToBytes(metadata);
|
|
200
|
+
if (bytes.length >= 38 && (bytes[0] === MAGIC_TRUST || bytes[0] === MAGIC_REVOKE)) {
|
|
201
|
+
return parseBinaryMetadata(bytes);
|
|
202
|
+
}
|
|
203
|
+
} catch (_) {
|
|
204
|
+
// not base64
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseBinaryMetadata(buf) {
|
|
212
|
+
if (!buf || buf.length < 38) return null;
|
|
213
|
+
|
|
214
|
+
if (buf[0] === MAGIC_TRUST && buf[1] === VERSION && buf.length >= 39) {
|
|
215
|
+
const level = buf[2];
|
|
216
|
+
if (level < 1 || level > 5) return null;
|
|
217
|
+
const timestamp = readUint32(buf, 3) * 1000;
|
|
218
|
+
const recipientPubkey = bytesToHex(buf.slice(7));
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
type: 'trust',
|
|
222
|
+
version: String(buf[1]),
|
|
223
|
+
level,
|
|
224
|
+
timestamp,
|
|
225
|
+
recipientPubkey,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (buf[0] === MAGIC_REVOKE && buf[1] === VERSION && buf.length >= 38) {
|
|
230
|
+
const timestamp = readUint32(buf, 2) * 1000;
|
|
231
|
+
const originalTxHash = bytesToHex(buf.slice(6, 38));
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
type: 'revocation',
|
|
235
|
+
version: String(buf[1]),
|
|
236
|
+
originalTxHash,
|
|
237
|
+
timestamp,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseLegacyMetadata(str) {
|
|
245
|
+
const parts = str.split(':');
|
|
246
|
+
|
|
247
|
+
if (parts[0] === LEGACY_TRUST_PREFIX && parts.length >= 5) {
|
|
248
|
+
return {
|
|
249
|
+
type: 'trust',
|
|
250
|
+
version: parts[1],
|
|
251
|
+
level: parseInt(parts[2], 10),
|
|
252
|
+
timestamp: parseInt(parts[3], 10),
|
|
253
|
+
recipientPubkey: parts[4],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (parts[0] === LEGACY_REVOKE_PREFIX && parts.length >= 4) {
|
|
258
|
+
return {
|
|
259
|
+
type: 'revocation',
|
|
260
|
+
version: parts[1],
|
|
261
|
+
originalTxHash: parts[2],
|
|
262
|
+
timestamp: parseInt(parts[3], 10),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// =============================================================================
|
|
270
|
+
// Bitcoin OP_RETURN Trust Transactions
|
|
271
|
+
// =============================================================================
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Build Bitcoin OP_RETURN output data for trust transaction.
|
|
275
|
+
* Uses compact binary encoding; total payload is 40-41 bytes (well within 80-byte limit).
|
|
276
|
+
*/
|
|
277
|
+
export function buildBitcoinTrustOpReturn(level, recipientPubkey) {
|
|
278
|
+
const bytes = encodeTrustMetadata(level, recipientPubkey);
|
|
279
|
+
|
|
280
|
+
if (bytes.length > 80) {
|
|
281
|
+
throw new Error('Trust metadata exceeds OP_RETURN size limit (80 bytes)');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// OP_RETURN format: 0x6a (OP_RETURN) + length + data
|
|
285
|
+
return {
|
|
286
|
+
scriptPubKey: `6a${bytes.length.toString(16).padStart(2, '0')}${bytesToHex(bytes)}`,
|
|
287
|
+
metadata: bytes,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Parse Bitcoin OP_RETURN data from transaction.
|
|
293
|
+
* Handles both binary and legacy ASCII payloads.
|
|
294
|
+
*/
|
|
295
|
+
export function parseBitcoinOpReturn(scriptPubKey) {
|
|
296
|
+
if (!scriptPubKey.startsWith('6a')) return null;
|
|
297
|
+
|
|
298
|
+
const dataHex = scriptPubKey.slice(4);
|
|
299
|
+
const bytes = hexToBytes(dataHex);
|
|
300
|
+
|
|
301
|
+
// Try binary first
|
|
302
|
+
if (bytes.length >= 38 && (bytes[0] === MAGIC_TRUST || bytes[0] === MAGIC_REVOKE)) {
|
|
303
|
+
return parseBinaryMetadata(bytes);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Fall back to legacy ASCII
|
|
307
|
+
const text = new TextDecoder().decode(bytes);
|
|
308
|
+
return parseLegacyMetadata(text);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// =============================================================================
|
|
312
|
+
// Solana Memo Trust Transactions
|
|
313
|
+
// =============================================================================
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Build Solana memo instruction for trust transaction.
|
|
317
|
+
* Returns base64-encoded binary for the memo field.
|
|
318
|
+
*/
|
|
319
|
+
export function buildSolanaTrustMemo(level, recipientPubkey) {
|
|
320
|
+
const bytes = encodeTrustMetadata(level, recipientPubkey);
|
|
321
|
+
return bytesToBase64(bytes);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Parse Solana memo from transaction.
|
|
326
|
+
* Handles both base64-encoded binary and legacy ASCII memos.
|
|
327
|
+
*/
|
|
328
|
+
export function parseSolanaMemo(memo) {
|
|
329
|
+
if (!memo) return null;
|
|
330
|
+
|
|
331
|
+
// Try base64 decode for binary format
|
|
332
|
+
try {
|
|
333
|
+
const bytes = base64ToBytes(memo);
|
|
334
|
+
if (bytes.length >= 38 && (bytes[0] === MAGIC_TRUST || bytes[0] === MAGIC_REVOKE)) {
|
|
335
|
+
return parseBinaryMetadata(bytes);
|
|
336
|
+
}
|
|
337
|
+
} catch (_) {
|
|
338
|
+
// not valid base64
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Fall back to legacy ASCII
|
|
342
|
+
return parseLegacyMetadata(memo);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// =============================================================================
|
|
346
|
+
// Ethereum Data Field Trust Transactions
|
|
347
|
+
// =============================================================================
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Build Ethereum transaction data field for trust transaction.
|
|
351
|
+
* Returns hex-encoded binary with 0x prefix.
|
|
352
|
+
*/
|
|
353
|
+
export function buildEthereumTrustData(level, recipientPubkey) {
|
|
354
|
+
const bytes = encodeTrustMetadata(level, recipientPubkey);
|
|
355
|
+
return '0x' + bytesToHex(bytes);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Parse Ethereum transaction data field.
|
|
360
|
+
* Handles both binary and legacy ASCII payloads.
|
|
361
|
+
*/
|
|
362
|
+
export function parseEthereumData(dataHex) {
|
|
363
|
+
if (!dataHex || dataHex === '0x') return null;
|
|
364
|
+
|
|
365
|
+
const bytes = hexToBytes(dataHex);
|
|
366
|
+
|
|
367
|
+
// Try binary first
|
|
368
|
+
if (bytes.length >= 38 && (bytes[0] === MAGIC_TRUST || bytes[0] === MAGIC_REVOKE)) {
|
|
369
|
+
return parseBinaryMetadata(bytes);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Fall back to legacy ASCII
|
|
373
|
+
const text = new TextDecoder().decode(bytes);
|
|
374
|
+
return parseLegacyMetadata(text);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// =============================================================================
|
|
378
|
+
// Trust Transaction Scanners
|
|
379
|
+
// =============================================================================
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Scan Bitcoin blockchain for trust transactions.
|
|
383
|
+
* Uses block explorer API to query OP_RETURN transactions.
|
|
384
|
+
*/
|
|
385
|
+
export async function scanBitcoinTrustTransactions(address) {
|
|
386
|
+
try {
|
|
387
|
+
const response = await fetch(`https://blockstream.info/api/address/${address}/txs`);
|
|
388
|
+
if (!response.ok) throw new Error('Failed to fetch Bitcoin transactions');
|
|
389
|
+
|
|
390
|
+
const txs = await response.json();
|
|
391
|
+
const trustTxs = [];
|
|
392
|
+
|
|
393
|
+
for (const tx of txs) {
|
|
394
|
+
for (const output of tx.vout) {
|
|
395
|
+
if (output.scriptpubkey_type === 'op_return') {
|
|
396
|
+
const parsed = parseBitcoinOpReturn(output.scriptpubkey);
|
|
397
|
+
if (parsed) {
|
|
398
|
+
trustTxs.push({
|
|
399
|
+
txHash: tx.txid,
|
|
400
|
+
blockHeight: tx.status.block_height,
|
|
401
|
+
timestamp: tx.status.block_time * 1000,
|
|
402
|
+
from: address,
|
|
403
|
+
chain: 'bitcoin',
|
|
404
|
+
...parsed,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return trustTxs;
|
|
412
|
+
} catch (err) {
|
|
413
|
+
console.error('Bitcoin trust scan failed:', err);
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Scan Solana blockchain for trust transactions.
|
|
420
|
+
* Uses RPC to get transactions with memo instructions.
|
|
421
|
+
*/
|
|
422
|
+
export async function scanSolanaTrustTransactions(address) {
|
|
423
|
+
try {
|
|
424
|
+
const response = await fetch('https://api.mainnet-beta.solana.com', {
|
|
425
|
+
method: 'POST',
|
|
426
|
+
headers: { 'Content-Type': 'application/json' },
|
|
427
|
+
body: JSON.stringify({
|
|
428
|
+
jsonrpc: '2.0',
|
|
429
|
+
id: 1,
|
|
430
|
+
method: 'getSignaturesForAddress',
|
|
431
|
+
params: [address, { limit: 100 }],
|
|
432
|
+
}),
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
if (!response.ok) throw new Error('Failed to fetch Solana signatures');
|
|
436
|
+
const data = await response.json();
|
|
437
|
+
const signatures = data.result || [];
|
|
438
|
+
|
|
439
|
+
const trustTxs = [];
|
|
440
|
+
|
|
441
|
+
for (const sig of signatures) {
|
|
442
|
+
const txResponse = await fetch('https://api.mainnet-beta.solana.com', {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
headers: { 'Content-Type': 'application/json' },
|
|
445
|
+
body: JSON.stringify({
|
|
446
|
+
jsonrpc: '2.0',
|
|
447
|
+
id: 1,
|
|
448
|
+
method: 'getTransaction',
|
|
449
|
+
params: [sig.signature, { encoding: 'jsonParsed' }],
|
|
450
|
+
}),
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (!txResponse.ok) continue;
|
|
454
|
+
const txData = await txResponse.json();
|
|
455
|
+
const tx = txData.result;
|
|
456
|
+
|
|
457
|
+
if (!tx || !tx.meta) continue;
|
|
458
|
+
|
|
459
|
+
const memos = tx.meta.logMessages?.filter(m => m.startsWith('Program log: Memo')) || [];
|
|
460
|
+
for (const memoLog of memos) {
|
|
461
|
+
const memo = memoLog.replace('Program log: Memo (len ', '').split('): "')[1]?.replace('"', '');
|
|
462
|
+
if (memo) {
|
|
463
|
+
const parsed = parseSolanaMemo(memo);
|
|
464
|
+
if (parsed) {
|
|
465
|
+
trustTxs.push({
|
|
466
|
+
txHash: sig.signature,
|
|
467
|
+
slot: tx.slot,
|
|
468
|
+
timestamp: (tx.blockTime || 0) * 1000,
|
|
469
|
+
from: address,
|
|
470
|
+
chain: 'solana',
|
|
471
|
+
...parsed,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return trustTxs;
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error('Solana trust scan failed:', err);
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Scan Ethereum blockchain for trust transactions.
|
|
487
|
+
* Uses Etherscan API to query 0-value transactions with data.
|
|
488
|
+
*/
|
|
489
|
+
export async function scanEthereumTrustTransactions(address) {
|
|
490
|
+
try {
|
|
491
|
+
console.warn('Ethereum trust scanning requires Etherscan API key');
|
|
492
|
+
return [];
|
|
493
|
+
} catch (err) {
|
|
494
|
+
console.error('Ethereum trust scan failed:', err);
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// =============================================================================
|
|
500
|
+
// Trust Relationship Analyzer
|
|
501
|
+
// =============================================================================
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Analyze trust relationships from a set of transactions relative to own addresses.
|
|
505
|
+
*
|
|
506
|
+
* Groups transactions by counterparty address and determines direction:
|
|
507
|
+
* - 'outbound': we sent trust to them
|
|
508
|
+
* - 'inbound': they sent trust to us
|
|
509
|
+
* - 'mutual': both directions exist
|
|
510
|
+
*
|
|
511
|
+
* @param {string[]} ownAddresses - Array of addresses belonging to the user
|
|
512
|
+
* @param {object[]} transactions - Array of trust transaction objects (from scanners)
|
|
513
|
+
* @returns {object[]} Array of relationship summaries
|
|
514
|
+
*/
|
|
515
|
+
export function analyzeTrustRelationships(ownAddresses, transactions) {
|
|
516
|
+
const addrs = Array.isArray(ownAddresses) ? ownAddresses : Object.values(ownAddresses || {}).filter(Boolean);
|
|
517
|
+
const ownSet = new Set(addrs.map(a => a.toLowerCase()));
|
|
518
|
+
const counterparties = new Map(); // address -> { outbound: [], inbound: [] }
|
|
519
|
+
|
|
520
|
+
for (const tx of transactions) {
|
|
521
|
+
if (tx.type !== 'trust') continue;
|
|
522
|
+
|
|
523
|
+
const fromAddr = (tx.from || '').toLowerCase();
|
|
524
|
+
const toAddr = (tx.recipientPubkey || '').toLowerCase();
|
|
525
|
+
const isFromUs = ownSet.has(fromAddr);
|
|
526
|
+
const isToUs = ownSet.has(toAddr);
|
|
527
|
+
|
|
528
|
+
const txRecord = {
|
|
529
|
+
txHash: tx.txHash,
|
|
530
|
+
timestamp: tx.timestamp,
|
|
531
|
+
level: tx.level,
|
|
532
|
+
type: tx.type,
|
|
533
|
+
chain: tx.chain || 'unknown',
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
if (isFromUs && !isToUs) {
|
|
537
|
+
// Outbound: we sent trust to them
|
|
538
|
+
const key = tx.recipientPubkey;
|
|
539
|
+
if (!counterparties.has(key)) {
|
|
540
|
+
counterparties.set(key, { outbound: [], inbound: [] });
|
|
541
|
+
}
|
|
542
|
+
counterparties.get(key).outbound.push({ ...txRecord, direction: 'outbound' });
|
|
543
|
+
} else if (!isFromUs && isToUs) {
|
|
544
|
+
// Inbound: they sent trust to us
|
|
545
|
+
const key = tx.from;
|
|
546
|
+
if (!counterparties.has(key)) {
|
|
547
|
+
counterparties.set(key, { outbound: [], inbound: [] });
|
|
548
|
+
}
|
|
549
|
+
counterparties.get(key).inbound.push({ ...txRecord, direction: 'inbound' });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const results = [];
|
|
554
|
+
|
|
555
|
+
for (const [address, data] of counterparties) {
|
|
556
|
+
const allTxs = [...data.outbound, ...data.inbound];
|
|
557
|
+
allTxs.sort((a, b) => b.timestamp - a.timestamp);
|
|
558
|
+
|
|
559
|
+
let direction;
|
|
560
|
+
if (data.outbound.length > 0 && data.inbound.length > 0) {
|
|
561
|
+
direction = 'mutual';
|
|
562
|
+
} else if (data.outbound.length > 0) {
|
|
563
|
+
direction = 'outbound';
|
|
564
|
+
} else {
|
|
565
|
+
direction = 'inbound';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Use the most recent trust level
|
|
569
|
+
const latest = allTxs[0];
|
|
570
|
+
|
|
571
|
+
results.push({
|
|
572
|
+
address,
|
|
573
|
+
chain: latest.chain,
|
|
574
|
+
level: latest.level,
|
|
575
|
+
direction,
|
|
576
|
+
txCount: allTxs.length,
|
|
577
|
+
lastSeen: latest.timestamp,
|
|
578
|
+
transactions: allTxs,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Sort by most recently seen
|
|
583
|
+
results.sort((a, b) => b.lastSeen - a.lastSeen);
|
|
584
|
+
|
|
585
|
+
return results;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// =============================================================================
|
|
589
|
+
// Trust Graph Builder
|
|
590
|
+
// =============================================================================
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Build trust graph from scanned transactions.
|
|
594
|
+
* Returns nodes (pubkeys) and edges (trust relationships).
|
|
595
|
+
*/
|
|
596
|
+
export function buildTrustGraph(trustTxs) {
|
|
597
|
+
const nodes = new Map();
|
|
598
|
+
const edges = [];
|
|
599
|
+
const revocations = new Map();
|
|
600
|
+
|
|
601
|
+
// First pass: collect revocations
|
|
602
|
+
for (const tx of trustTxs) {
|
|
603
|
+
if (tx.type === 'revocation') {
|
|
604
|
+
revocations.set(tx.originalTxHash, tx.timestamp);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Second pass: build graph
|
|
609
|
+
for (const tx of trustTxs) {
|
|
610
|
+
if (tx.type === 'trust') {
|
|
611
|
+
if (!nodes.has(tx.from)) {
|
|
612
|
+
nodes.set(tx.from, {
|
|
613
|
+
id: tx.from,
|
|
614
|
+
label: truncatePubkey(tx.from),
|
|
615
|
+
ownKey: false,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (!nodes.has(tx.recipientPubkey)) {
|
|
620
|
+
nodes.set(tx.recipientPubkey, {
|
|
621
|
+
id: tx.recipientPubkey,
|
|
622
|
+
label: truncatePubkey(tx.recipientPubkey),
|
|
623
|
+
ownKey: false,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const revoked = revocations.has(tx.txHash);
|
|
628
|
+
edges.push({
|
|
629
|
+
from: tx.from,
|
|
630
|
+
to: tx.recipientPubkey,
|
|
631
|
+
level: tx.level,
|
|
632
|
+
txHash: tx.txHash,
|
|
633
|
+
timestamp: tx.timestamp,
|
|
634
|
+
revoked,
|
|
635
|
+
revokedAt: revoked ? revocations.get(tx.txHash) : null,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
nodes: Array.from(nodes.values()),
|
|
642
|
+
edges,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Calculate trust score for a pubkey based on graph.
|
|
648
|
+
* Implements weighted transitive trust (web of trust).
|
|
649
|
+
*/
|
|
650
|
+
export function calculateTrustScore(graph, targetPubkey, ownPubkeys = []) {
|
|
651
|
+
let score = 0;
|
|
652
|
+
|
|
653
|
+
// Direct trust from own keys
|
|
654
|
+
const directEdges = graph.edges.filter(
|
|
655
|
+
e => ownPubkeys.includes(e.from) && e.to === targetPubkey && !e.revoked
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
for (const edge of directEdges) {
|
|
659
|
+
score += edge.level * 20; // Max 100 for ULTIMATE (5 * 20)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Transitive trust (2nd degree)
|
|
663
|
+
const secondDegreeNodes = graph.edges
|
|
664
|
+
.filter(e => ownPubkeys.includes(e.from) && !e.revoked && e.level >= TrustLevel.FULL)
|
|
665
|
+
.map(e => e.to);
|
|
666
|
+
|
|
667
|
+
for (const intermediateNode of secondDegreeNodes) {
|
|
668
|
+
const transitiveEdges = graph.edges.filter(
|
|
669
|
+
e => e.from === intermediateNode && e.to === targetPubkey && !e.revoked
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
for (const edge of transitiveEdges) {
|
|
673
|
+
score += edge.level * 20 * 0.5;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Boomerang trust bonus (bidirectional)
|
|
678
|
+
const outgoing = graph.edges.filter(e => e.from === targetPubkey && !e.revoked);
|
|
679
|
+
const incoming = graph.edges.filter(e => e.to === targetPubkey && !e.revoked);
|
|
680
|
+
|
|
681
|
+
const bidirectional = outgoing.filter(out =>
|
|
682
|
+
incoming.some(inc => inc.from === out.to)
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
if (bidirectional.length > 0) {
|
|
686
|
+
score += 10;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return Math.min(Math.round(score), 100);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// =============================================================================
|
|
693
|
+
// Helpers
|
|
694
|
+
// =============================================================================
|
|
695
|
+
|
|
696
|
+
function truncatePubkey(pubkey, prefixLen = 8, suffixLen = 6) {
|
|
697
|
+
if (pubkey.length <= prefixLen + suffixLen + 3) return pubkey;
|
|
698
|
+
return `${pubkey.slice(0, prefixLen)}...${pubkey.slice(-suffixLen)}`;
|
|
699
|
+
}
|