tx-indexer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1320 @@
1
+ import { createSolanaRpc, createSolanaRpcSubscriptions, address, signature } from '@solana/kit';
2
+
3
+ // ../solana/src/rpc/client.ts
4
+ function createSolanaClient(rpcUrl, wsUrl) {
5
+ const rpc = createSolanaRpc(rpcUrl);
6
+ const rpcSubscriptions = wsUrl ? createSolanaRpcSubscriptions(wsUrl) : createSolanaRpcSubscriptions(rpcUrl.replace("https://", "wss://"));
7
+ return { rpc, rpcSubscriptions };
8
+ }
9
+ function parseAddress(addr) {
10
+ return address(addr);
11
+ }
12
+ function parseSignature(sig) {
13
+ return signature(sig);
14
+ }
15
+
16
+ // ../domain/src/money/token-registry.ts
17
+ var KNOWN_TOKENS = {
18
+ SOL: "So11111111111111111111111111111111111111112",
19
+ USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
20
+ USDC_BRIDGED: "A9mUU4qviSctJVPJdBJWkb28deg915LYJKrzQ19ji3FM",
21
+ USDG: "2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH"
22
+ };
23
+ var TOKEN_INFO = {
24
+ [KNOWN_TOKENS.SOL]: {
25
+ mint: KNOWN_TOKENS.SOL,
26
+ symbol: "SOL",
27
+ name: "Solana",
28
+ decimals: 9,
29
+ logoURI: "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png"
30
+ },
31
+ [KNOWN_TOKENS.USDC]: {
32
+ mint: KNOWN_TOKENS.USDC,
33
+ symbol: "USDC",
34
+ name: "USD Coin",
35
+ decimals: 6,
36
+ logoURI: "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png"
37
+ },
38
+ [KNOWN_TOKENS.USDC_BRIDGED]: {
39
+ mint: KNOWN_TOKENS.USDC_BRIDGED,
40
+ symbol: "USDCet",
41
+ name: "USDC (Bridged)",
42
+ decimals: 6
43
+ },
44
+ [KNOWN_TOKENS.USDG]: {
45
+ mint: KNOWN_TOKENS.USDG,
46
+ symbol: "USDG",
47
+ name: "USD Glitter",
48
+ decimals: 6
49
+ }
50
+ };
51
+ function getTokenInfo(mint) {
52
+ return TOKEN_INFO[mint];
53
+ }
54
+ [
55
+ KNOWN_TOKENS.USDC,
56
+ KNOWN_TOKENS.USDC_BRIDGED,
57
+ KNOWN_TOKENS.USDG
58
+ ];
59
+ [
60
+ KNOWN_TOKENS.SOL,
61
+ KNOWN_TOKENS.USDC
62
+ ];
63
+
64
+ // ../solana/src/fetcher/balances.ts
65
+ async function fetchWalletBalance(rpc, walletAddress, tokenMints) {
66
+ const balanceResponse = await rpc.getBalance(walletAddress).send();
67
+ const lamports = balanceResponse.value;
68
+ const tokenAccounts = await fetchTokenAccounts(rpc, walletAddress);
69
+ const tokens = tokenMints ? filterToTrackedTokens(tokenAccounts, tokenMints) : Array.from(tokenAccounts.values());
70
+ return {
71
+ address: walletAddress,
72
+ sol: {
73
+ lamports,
74
+ ui: Number(lamports) / 1e9
75
+ },
76
+ tokens
77
+ };
78
+ }
79
+ async function fetchTokenAccounts(rpc, walletAddress) {
80
+ const accountsMap = /* @__PURE__ */ new Map();
81
+ try {
82
+ const response = await rpc.getTokenAccountsByOwner(
83
+ walletAddress,
84
+ { programId: address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") },
85
+ { encoding: "jsonParsed" }
86
+ ).send();
87
+ for (const account of response.value) {
88
+ const parsedInfo = account.account.data.parsed.info;
89
+ const mint = parsedInfo.mint;
90
+ const tokenInfo = getTokenInfo(mint);
91
+ const symbol = tokenInfo?.symbol || mint.substring(0, 8);
92
+ accountsMap.set(mint, {
93
+ mint,
94
+ tokenAccount: account.pubkey.toString(),
95
+ amount: {
96
+ raw: parsedInfo.tokenAmount.amount,
97
+ ui: parsedInfo.tokenAmount.uiAmountString ? parseFloat(parsedInfo.tokenAmount.uiAmountString) : 0
98
+ },
99
+ decimals: parsedInfo.tokenAmount.decimals,
100
+ symbol
101
+ });
102
+ }
103
+ } catch (error) {
104
+ console.error("Error fetching token accounts:", error);
105
+ }
106
+ return accountsMap;
107
+ }
108
+ function filterToTrackedTokens(fetchedAccounts, tokenMints) {
109
+ const result = [];
110
+ for (const mint of tokenMints) {
111
+ if (mint === KNOWN_TOKENS.SOL) continue;
112
+ const existing = fetchedAccounts.get(mint);
113
+ if (existing) {
114
+ result.push(existing);
115
+ } else {
116
+ const tokenInfo = getTokenInfo(mint);
117
+ if (tokenInfo) {
118
+ result.push({
119
+ mint,
120
+ amount: {
121
+ raw: "0",
122
+ ui: 0
123
+ },
124
+ decimals: tokenInfo.decimals,
125
+ symbol: tokenInfo.symbol
126
+ });
127
+ }
128
+ }
129
+ }
130
+ return result;
131
+ }
132
+
133
+ // ../solana/src/mappers/transaction-mapper.ts
134
+ function extractProgramIds(transaction) {
135
+ const programIds = /* @__PURE__ */ new Set();
136
+ const { message } = transaction;
137
+ const { accountKeys, instructions } = message;
138
+ for (const ix of instructions) {
139
+ const { programIdIndex } = ix;
140
+ if (programIdIndex !== void 0 && accountKeys[programIdIndex]) {
141
+ const key = accountKeys[programIdIndex];
142
+ programIds.add(key.toString());
143
+ }
144
+ }
145
+ return Array.from(programIds);
146
+ }
147
+
148
+ // ../solana/src/constants/program-ids.ts
149
+ var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
150
+ var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
151
+ var STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111";
152
+ var TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
153
+ var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
154
+ var SPL_MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
155
+ var MEMO_V1_PROGRAM_ID = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
156
+ var JUPITER_V6_PROGRAM_ID = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4";
157
+ var JUPITER_V4_PROGRAM_ID = "JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB";
158
+ var RAYDIUM_PROGRAM_ID = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
159
+ var ORCA_WHIRLPOOL_PROGRAM_ID = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc";
160
+ var METAPLEX_PROGRAM_ID = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
161
+ var PAYAI_FACILITATOR = "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4";
162
+ var KNOWN_FACILITATORS = [
163
+ PAYAI_FACILITATOR
164
+ ];
165
+ function detectFacilitator(accountKeys) {
166
+ for (const facilitator of KNOWN_FACILITATORS) {
167
+ if (accountKeys.includes(facilitator)) {
168
+ if (facilitator === PAYAI_FACILITATOR) {
169
+ return "payai";
170
+ }
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+
176
+ // ../../node_modules/.bun/base-x@5.0.1/node_modules/base-x/src/esm/index.js
177
+ function base(ALPHABET2) {
178
+ if (ALPHABET2.length >= 255) {
179
+ throw new TypeError("Alphabet too long");
180
+ }
181
+ const BASE_MAP = new Uint8Array(256);
182
+ for (let j = 0; j < BASE_MAP.length; j++) {
183
+ BASE_MAP[j] = 255;
184
+ }
185
+ for (let i = 0; i < ALPHABET2.length; i++) {
186
+ const x = ALPHABET2.charAt(i);
187
+ const xc = x.charCodeAt(0);
188
+ if (BASE_MAP[xc] !== 255) {
189
+ throw new TypeError(x + " is ambiguous");
190
+ }
191
+ BASE_MAP[xc] = i;
192
+ }
193
+ const BASE = ALPHABET2.length;
194
+ const LEADER = ALPHABET2.charAt(0);
195
+ const FACTOR = Math.log(BASE) / Math.log(256);
196
+ const iFACTOR = Math.log(256) / Math.log(BASE);
197
+ function encode(source) {
198
+ if (source instanceof Uint8Array) ; else if (ArrayBuffer.isView(source)) {
199
+ source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
200
+ } else if (Array.isArray(source)) {
201
+ source = Uint8Array.from(source);
202
+ }
203
+ if (!(source instanceof Uint8Array)) {
204
+ throw new TypeError("Expected Uint8Array");
205
+ }
206
+ if (source.length === 0) {
207
+ return "";
208
+ }
209
+ let zeroes = 0;
210
+ let length = 0;
211
+ let pbegin = 0;
212
+ const pend = source.length;
213
+ while (pbegin !== pend && source[pbegin] === 0) {
214
+ pbegin++;
215
+ zeroes++;
216
+ }
217
+ const size = (pend - pbegin) * iFACTOR + 1 >>> 0;
218
+ const b58 = new Uint8Array(size);
219
+ while (pbegin !== pend) {
220
+ let carry = source[pbegin];
221
+ let i = 0;
222
+ for (let it1 = size - 1; (carry !== 0 || i < length) && it1 !== -1; it1--, i++) {
223
+ carry += 256 * b58[it1] >>> 0;
224
+ b58[it1] = carry % BASE >>> 0;
225
+ carry = carry / BASE >>> 0;
226
+ }
227
+ if (carry !== 0) {
228
+ throw new Error("Non-zero carry");
229
+ }
230
+ length = i;
231
+ pbegin++;
232
+ }
233
+ let it2 = size - length;
234
+ while (it2 !== size && b58[it2] === 0) {
235
+ it2++;
236
+ }
237
+ let str = LEADER.repeat(zeroes);
238
+ for (; it2 < size; ++it2) {
239
+ str += ALPHABET2.charAt(b58[it2]);
240
+ }
241
+ return str;
242
+ }
243
+ function decodeUnsafe(source) {
244
+ if (typeof source !== "string") {
245
+ throw new TypeError("Expected String");
246
+ }
247
+ if (source.length === 0) {
248
+ return new Uint8Array();
249
+ }
250
+ let psz = 0;
251
+ let zeroes = 0;
252
+ let length = 0;
253
+ while (source[psz] === LEADER) {
254
+ zeroes++;
255
+ psz++;
256
+ }
257
+ const size = (source.length - psz) * FACTOR + 1 >>> 0;
258
+ const b256 = new Uint8Array(size);
259
+ while (psz < source.length) {
260
+ const charCode = source.charCodeAt(psz);
261
+ if (charCode > 255) {
262
+ return;
263
+ }
264
+ let carry = BASE_MAP[charCode];
265
+ if (carry === 255) {
266
+ return;
267
+ }
268
+ let i = 0;
269
+ for (let it3 = size - 1; (carry !== 0 || i < length) && it3 !== -1; it3--, i++) {
270
+ carry += BASE * b256[it3] >>> 0;
271
+ b256[it3] = carry % 256 >>> 0;
272
+ carry = carry / 256 >>> 0;
273
+ }
274
+ if (carry !== 0) {
275
+ throw new Error("Non-zero carry");
276
+ }
277
+ length = i;
278
+ psz++;
279
+ }
280
+ let it4 = size - length;
281
+ while (it4 !== size && b256[it4] === 0) {
282
+ it4++;
283
+ }
284
+ const vch = new Uint8Array(zeroes + (size - it4));
285
+ let j = zeroes;
286
+ while (it4 !== size) {
287
+ vch[j++] = b256[it4++];
288
+ }
289
+ return vch;
290
+ }
291
+ function decode(string) {
292
+ const buffer = decodeUnsafe(string);
293
+ if (buffer) {
294
+ return buffer;
295
+ }
296
+ throw new Error("Non-base" + BASE + " character");
297
+ }
298
+ return {
299
+ encode,
300
+ decodeUnsafe,
301
+ decode
302
+ };
303
+ }
304
+ var esm_default = base;
305
+
306
+ // ../../node_modules/.bun/bs58@6.0.0/node_modules/bs58/src/esm/index.js
307
+ var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
308
+ var esm_default2 = esm_default(ALPHABET);
309
+
310
+ // ../solana/src/mappers/memo-parser.ts
311
+ function decodeMemoData(base64Data) {
312
+ const buffer = Buffer.from(base64Data, "base64");
313
+ const text = buffer.toString("utf-8");
314
+ if (!text.includes("\uFFFD") && /^[\x20-\x7E\s]*$/.test(text)) {
315
+ return text;
316
+ }
317
+ if (buffer.length === 32) {
318
+ return esm_default2.encode(buffer);
319
+ }
320
+ if (buffer.length % 32 === 0 && buffer.length > 0) {
321
+ const addresses = [];
322
+ for (let i = 0; i < buffer.length; i += 32) {
323
+ const chunk = buffer.subarray(i, i + 32);
324
+ addresses.push(esm_default2.encode(chunk));
325
+ }
326
+ return addresses.join(", ");
327
+ }
328
+ if (buffer.length >= 16) {
329
+ const uuidBytes = buffer.subarray(0, 16);
330
+ const uuid = [
331
+ uuidBytes.subarray(0, 4).toString("hex"),
332
+ uuidBytes.subarray(4, 6).toString("hex"),
333
+ uuidBytes.subarray(6, 8).toString("hex"),
334
+ uuidBytes.subarray(8, 10).toString("hex"),
335
+ uuidBytes.subarray(10, 16).toString("hex")
336
+ ].join("-");
337
+ if (buffer.length === 16) {
338
+ return `Product: ${uuid}`;
339
+ }
340
+ const extraData = buffer.subarray(16);
341
+ const extraHex = extraData.toString("hex");
342
+ return `Product: ${uuid} | Meta: ${extraHex}`;
343
+ }
344
+ return esm_default2.encode(buffer);
345
+ }
346
+ function extractMemo(transaction) {
347
+ if (transaction.meta?.logMessages) {
348
+ const memoLogPattern = /Program log: Memo \(len \d+\): "(.+)"/;
349
+ for (const log of transaction.meta.logMessages) {
350
+ const match = log.match(memoLogPattern);
351
+ if (match?.[1]) {
352
+ return match[1];
353
+ }
354
+ }
355
+ }
356
+ const { message } = transaction;
357
+ const { accountKeys, instructions } = message;
358
+ for (const ix of instructions) {
359
+ const programId = accountKeys[ix.programIdIndex]?.toString();
360
+ if (programId === SPL_MEMO_PROGRAM_ID || programId === MEMO_V1_PROGRAM_ID) {
361
+ if (ix.data) {
362
+ try {
363
+ return decodeMemoData(ix.data);
364
+ } catch (e) {
365
+ console.warn("Failed to decode memo:", e);
366
+ }
367
+ }
368
+ }
369
+ }
370
+ return null;
371
+ }
372
+ function parseSolanaPayMemo(memo) {
373
+ try {
374
+ const parsed = JSON.parse(memo);
375
+ return { ...parsed, raw: memo };
376
+ } catch {
377
+ return { raw: memo };
378
+ }
379
+ }
380
+ function isSolanaPayTransaction(programIds, memo) {
381
+ const hasMemoProgram = programIds.includes(SPL_MEMO_PROGRAM_ID) || programIds.includes(MEMO_V1_PROGRAM_ID);
382
+ return hasMemoProgram && memo !== null && memo !== void 0;
383
+ }
384
+
385
+ // ../solana/src/fetcher/transactions.ts
386
+ async function fetchWalletSignatures(rpc, walletAddress, config = {}) {
387
+ const { limit = 1e3, before, until } = config;
388
+ const response = await rpc.getSignaturesForAddress(walletAddress, {
389
+ limit,
390
+ before,
391
+ until
392
+ }).send();
393
+ return response.map((sig) => ({
394
+ signature: sig.signature,
395
+ slot: sig.slot,
396
+ blockTime: sig.blockTime,
397
+ err: sig.err,
398
+ programIds: [],
399
+ protocol: null,
400
+ memo: sig.memo || null
401
+ }));
402
+ }
403
+ async function fetchTransaction(rpc, signature2, commitment = "confirmed") {
404
+ const response = await rpc.getTransaction(signature2, {
405
+ commitment,
406
+ maxSupportedTransactionVersion: 0,
407
+ encoding: "json"
408
+ }).send();
409
+ if (!response) {
410
+ return null;
411
+ }
412
+ const transactionWithLogs = {
413
+ ...response.transaction,
414
+ meta: { logMessages: response.meta?.logMessages }
415
+ };
416
+ const memo = extractMemo(transactionWithLogs);
417
+ return {
418
+ signature: signature2,
419
+ slot: response.slot,
420
+ blockTime: response.blockTime,
421
+ err: response.meta?.err ?? null,
422
+ programIds: extractProgramIds(response.transaction),
423
+ protocol: null,
424
+ preTokenBalances: (response.meta?.preTokenBalances ?? []).map((bal) => ({
425
+ accountIndex: bal.accountIndex,
426
+ mint: bal.mint.toString(),
427
+ owner: bal.owner?.toString(),
428
+ programId: bal.programId?.toString(),
429
+ uiTokenAmount: {
430
+ amount: bal.uiTokenAmount.amount.toString(),
431
+ decimals: bal.uiTokenAmount.decimals,
432
+ uiAmountString: bal.uiTokenAmount.uiAmountString.toString()
433
+ }
434
+ })),
435
+ postTokenBalances: (response.meta?.postTokenBalances ?? []).map((bal) => ({
436
+ accountIndex: bal.accountIndex,
437
+ mint: bal.mint.toString(),
438
+ owner: bal.owner?.toString(),
439
+ programId: bal.programId?.toString(),
440
+ uiTokenAmount: {
441
+ amount: bal.uiTokenAmount.amount.toString(),
442
+ decimals: bal.uiTokenAmount.decimals,
443
+ uiAmountString: bal.uiTokenAmount.uiAmountString.toString()
444
+ }
445
+ })),
446
+ preBalances: (response.meta?.preBalances ?? []).map((bal) => Number(bal)),
447
+ postBalances: (response.meta?.postBalances ?? []).map((bal) => Number(bal)),
448
+ accountKeys: response.transaction.message.accountKeys.map(
449
+ (key) => key.toString()
450
+ ),
451
+ memo
452
+ };
453
+ }
454
+ async function fetchTransactionsBatch(rpc, signatures, commitment = "confirmed") {
455
+ const promises = signatures.map(
456
+ (sig) => fetchTransaction(rpc, sig, commitment)
457
+ );
458
+ const results = await Promise.all(promises);
459
+ return results.filter((tx) => tx !== null);
460
+ }
461
+
462
+ // ../domain/src/tx/account-id.ts
463
+ function buildAccountId(params) {
464
+ const { type, address: address3, protocol, token } = params;
465
+ if (type === "wallet") {
466
+ return `wallet:${address3}`;
467
+ }
468
+ if (type === "protocol" && protocol) {
469
+ if (token) {
470
+ return `protocol:${protocol}:${token}:${address3}`;
471
+ }
472
+ return `protocol:${protocol}:${address3}`;
473
+ }
474
+ if (type === "external") {
475
+ return `external:${address3}`;
476
+ }
477
+ if (type === "fee") {
478
+ return "fee:network";
479
+ }
480
+ throw new Error(
481
+ `Invalid accountId parameters: type=${type}, address=${address3}, protocol=${protocol}`
482
+ );
483
+ }
484
+ function parseAccountId(accountId) {
485
+ const parts = accountId.split(":");
486
+ if (parts[0] === "wallet" && parts.length === 2) {
487
+ return { type: "wallet", address: parts[1] };
488
+ }
489
+ if (parts[0] === "protocol") {
490
+ if (parts.length === 4) {
491
+ return {
492
+ type: "protocol",
493
+ protocol: parts[1],
494
+ token: parts[2],
495
+ address: parts[3]
496
+ };
497
+ }
498
+ if (parts.length === 3) {
499
+ return {
500
+ type: "protocol",
501
+ protocol: parts[1],
502
+ address: parts[2]
503
+ };
504
+ }
505
+ }
506
+ if (parts[0] === "external" && parts.length === 2) {
507
+ return { type: "external", address: parts[1] };
508
+ }
509
+ if (parts[0] === "fee" && parts[1] === "network") {
510
+ return { type: "fee" };
511
+ }
512
+ return { type: "unknown", address: accountId };
513
+ }
514
+
515
+ // ../solana/src/mappers/balance-parser.ts
516
+ function extractTokenBalanceChanges(tx, filterMints) {
517
+ const { preTokenBalances = [], postTokenBalances = [] } = tx;
518
+ const changes = [];
519
+ const preBalanceMap = /* @__PURE__ */ new Map();
520
+ for (const bal of preTokenBalances) {
521
+ preBalanceMap.set(bal.accountIndex, bal);
522
+ }
523
+ for (const postBal of postTokenBalances) {
524
+ const { accountIndex, mint } = postBal;
525
+ let tokenInfo = getTokenInfo(mint);
526
+ if (!tokenInfo) {
527
+ tokenInfo = {
528
+ mint,
529
+ symbol: mint.slice(0, 8),
530
+ decimals: postBal.uiTokenAmount.decimals,
531
+ name: `Unknown Token (${mint.slice(0, 8)}...)`
532
+ };
533
+ }
534
+ const preBal = preBalanceMap.get(accountIndex);
535
+ const preAmount = preBal?.uiTokenAmount.amount ?? "0";
536
+ const postAmount = postBal.uiTokenAmount.amount;
537
+ const preUi = preBal?.uiTokenAmount.uiAmountString ? parseFloat(preBal.uiTokenAmount.uiAmountString) : 0;
538
+ const postUi = postBal.uiTokenAmount.uiAmountString ? parseFloat(postBal.uiTokenAmount.uiAmountString) : 0;
539
+ const changeRaw = (BigInt(postAmount) - BigInt(preAmount)).toString();
540
+ const changeUi = postUi - preUi;
541
+ if (changeRaw !== "0") {
542
+ changes.push({
543
+ mint,
544
+ tokenInfo,
545
+ accountIndex,
546
+ owner: postBal.owner,
547
+ preBalance: {
548
+ raw: preAmount,
549
+ ui: preUi
550
+ },
551
+ postBalance: {
552
+ raw: postAmount,
553
+ ui: postUi
554
+ },
555
+ change: {
556
+ raw: changeRaw,
557
+ ui: changeUi
558
+ }
559
+ });
560
+ }
561
+ }
562
+ return changes;
563
+ }
564
+ function extractSolBalanceChanges(tx) {
565
+ const {
566
+ preBalances = [],
567
+ postBalances = [],
568
+ accountKeys = []
569
+ } = tx;
570
+ const changes = [];
571
+ for (let i = 0; i < accountKeys.length; i++) {
572
+ const address3 = accountKeys[i];
573
+ if (!address3) continue;
574
+ const preBal = BigInt(preBalances[i] ?? 0);
575
+ const postBal = BigInt(postBalances[i] ?? 0);
576
+ const change = postBal - preBal;
577
+ if (change !== 0n) {
578
+ changes.push({
579
+ accountIndex: i,
580
+ address: address3,
581
+ preBalance: preBal,
582
+ postBalance: postBal,
583
+ change,
584
+ changeUi: Number(change) / 1e9
585
+ });
586
+ }
587
+ }
588
+ return changes;
589
+ }
590
+
591
+ // ../solana/src/constants/protocol-constants.ts
592
+ var DEX_PROTOCOL_IDS = /* @__PURE__ */ new Set([
593
+ "jupiter",
594
+ "jupiter-v4",
595
+ "raydium",
596
+ "orca-whirlpool"
597
+ ]);
598
+
599
+ // ../solana/src/mappers/transaction-to-legs.ts
600
+ function isDexProtocol(protocol) {
601
+ return protocol !== null && protocol !== void 0 && DEX_PROTOCOL_IDS.has(protocol.id);
602
+ }
603
+ function transactionToLegs(tx, walletAddress) {
604
+ const legs = [];
605
+ const feePayer = tx.accountKeys?.[0]?.toLowerCase();
606
+ const solChanges = extractSolBalanceChanges(tx);
607
+ let totalSolDebits = 0n;
608
+ let totalSolCredits = 0n;
609
+ for (const change of solChanges) {
610
+ if (change.change === 0n) continue;
611
+ const isWallet = walletAddress ? change.address.toLowerCase() === walletAddress.toLowerCase() : false;
612
+ const accountId = buildAccountId({
613
+ type: isWallet ? "wallet" : "external",
614
+ address: change.address
615
+ });
616
+ const solInfo = TOKEN_INFO[KNOWN_TOKENS.SOL];
617
+ if (!solInfo) {
618
+ throw new Error("SOL token info not found in registry");
619
+ }
620
+ if (change.change > 0n) {
621
+ totalSolCredits += change.change;
622
+ } else {
623
+ totalSolDebits += -change.change;
624
+ }
625
+ legs.push({
626
+ accountId,
627
+ side: change.change > 0n ? "credit" : "debit",
628
+ amount: {
629
+ token: solInfo,
630
+ amountRaw: change.change.toString().replace("-", ""),
631
+ amountUi: Math.abs(change.changeUi)
632
+ },
633
+ role: determineSolRole(change, walletAddress, tx, feePayer)
634
+ });
635
+ }
636
+ const networkFee = totalSolDebits - totalSolCredits;
637
+ if (networkFee > 0n) {
638
+ const solInfo = TOKEN_INFO[KNOWN_TOKENS.SOL];
639
+ if (solInfo) {
640
+ legs.push({
641
+ accountId: buildAccountId({ type: "fee", address: "" }),
642
+ side: "credit",
643
+ amount: {
644
+ token: solInfo,
645
+ amountRaw: networkFee.toString(),
646
+ amountUi: Number(networkFee) / 1e9
647
+ },
648
+ role: "fee"
649
+ });
650
+ }
651
+ }
652
+ const tokenChanges = extractTokenBalanceChanges(tx);
653
+ for (const change of tokenChanges) {
654
+ if (change.change.raw === "0") continue;
655
+ const isWallet = walletAddress ? change.owner?.toLowerCase() === walletAddress.toLowerCase() : false;
656
+ const isFeePayer = !walletAddress && feePayer ? change.owner?.toLowerCase() === feePayer : false;
657
+ let accountId;
658
+ if (isWallet && walletAddress) {
659
+ accountId = buildAccountId({
660
+ type: "wallet",
661
+ address: change.owner || walletAddress
662
+ });
663
+ } else if (isFeePayer && feePayer) {
664
+ accountId = buildAccountId({
665
+ type: "external",
666
+ address: change.owner || feePayer
667
+ });
668
+ } else if (isDexProtocol(tx.protocol) && walletAddress) {
669
+ accountId = buildAccountId({
670
+ type: "protocol",
671
+ address: change.owner || change.tokenInfo.mint,
672
+ protocol: tx.protocol.id,
673
+ token: change.tokenInfo.symbol
674
+ });
675
+ } else {
676
+ accountId = buildAccountId({
677
+ type: "external",
678
+ address: change.owner || change.tokenInfo.mint
679
+ });
680
+ }
681
+ legs.push({
682
+ accountId,
683
+ side: change.change.ui > 0 ? "credit" : "debit",
684
+ amount: {
685
+ token: change.tokenInfo,
686
+ amountRaw: change.change.raw.replace("-", ""),
687
+ amountUi: Math.abs(change.change.ui)
688
+ },
689
+ role: determineTokenRole(change, walletAddress, tx, feePayer)
690
+ });
691
+ }
692
+ return legs;
693
+ }
694
+ function determineSolRole(change, walletAddress, tx, feePayer) {
695
+ const isWallet = walletAddress ? change.address.toLowerCase() === walletAddress.toLowerCase() : false;
696
+ const isFeePayer = !walletAddress && feePayer ? change.address.toLowerCase() === feePayer : false;
697
+ const isPositive = change.change > 0n;
698
+ const amountSol = Math.abs(change.changeUi);
699
+ if (isWallet) {
700
+ if (!isPositive && amountSol < 0.01) {
701
+ return "fee";
702
+ }
703
+ if (isPositive) {
704
+ if (tx.protocol?.id === "stake") {
705
+ return "reward";
706
+ }
707
+ return "received";
708
+ }
709
+ return "sent";
710
+ }
711
+ if (isFeePayer && !isPositive && amountSol < 0.01) {
712
+ return "fee";
713
+ }
714
+ if (isPositive) {
715
+ return "received";
716
+ }
717
+ return "sent";
718
+ }
719
+ function determineTokenRole(change, walletAddress, tx, feePayer) {
720
+ const isWallet = walletAddress ? change.owner?.toLowerCase() === walletAddress.toLowerCase() : false;
721
+ const isFeePayer = !walletAddress && feePayer ? change.owner?.toLowerCase() === feePayer : false;
722
+ const isPositive = change.change.ui > 0;
723
+ if (isWallet || isFeePayer) {
724
+ return isPositive ? "received" : "sent";
725
+ }
726
+ if (isDexProtocol(tx.protocol)) {
727
+ return isPositive ? "protocol_withdraw" : "protocol_deposit";
728
+ }
729
+ return isPositive ? "received" : "sent";
730
+ }
731
+
732
+ // ../classification/src/classifiers/transfer-classifier.ts
733
+ var TransferClassifier = class {
734
+ name = "transfer";
735
+ priority = 20;
736
+ classify(context) {
737
+ const { legs, walletAddress, tx } = context;
738
+ const facilitator = tx.accountKeys ? detectFacilitator(tx.accountKeys) : null;
739
+ const isObserverMode = !walletAddress;
740
+ let participantAddress;
741
+ let participantPrefix;
742
+ if (walletAddress) {
743
+ participantAddress = walletAddress.toLowerCase();
744
+ participantPrefix = `wallet:${participantAddress}`;
745
+ } else {
746
+ const feeLeg = legs.find(
747
+ (leg) => leg.role === "fee" && leg.accountId.startsWith("external:")
748
+ );
749
+ if (!feeLeg) return null;
750
+ participantAddress = feeLeg.accountId.replace("external:", "").toLowerCase();
751
+ participantPrefix = `external:${participantAddress}`;
752
+ }
753
+ const participantSent = legs.filter(
754
+ (l) => l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.side === "debit" && l.role === "sent"
755
+ );
756
+ const participantReceived = legs.filter(
757
+ (l) => l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.side === "credit" && l.role === "received"
758
+ );
759
+ const otherReceived = legs.filter(
760
+ (l) => !l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.accountId.startsWith("external:") && l.side === "credit" && l.role === "received"
761
+ );
762
+ const otherSent = legs.filter(
763
+ (l) => !l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.accountId.startsWith("external:") && l.side === "debit" && l.role === "sent"
764
+ );
765
+ for (const sent of participantSent) {
766
+ const matchingReceived = otherReceived.find(
767
+ (r) => r.amount.token.mint === sent.amount.token.mint
768
+ );
769
+ if (matchingReceived) {
770
+ const receiverAddress = matchingReceived.accountId.replace("external:", "");
771
+ return {
772
+ primaryType: "transfer",
773
+ direction: isObserverMode ? "neutral" : "outgoing",
774
+ primaryAmount: sent.amount,
775
+ secondaryAmount: null,
776
+ counterparty: {
777
+ type: "unknown",
778
+ address: receiverAddress,
779
+ name: `${receiverAddress.slice(0, 8)}...`
780
+ },
781
+ confidence: isObserverMode ? 0.9 : 0.95,
782
+ isRelevant: true,
783
+ metadata: {
784
+ ...isObserverMode && { observer_mode: true, sender: participantAddress },
785
+ ...facilitator && { facilitator, payment_type: "facilitated" }
786
+ }
787
+ };
788
+ }
789
+ }
790
+ for (const received of participantReceived) {
791
+ const matchingSent = otherSent.find(
792
+ (s) => s.amount.token.mint === received.amount.token.mint
793
+ );
794
+ if (matchingSent) {
795
+ const senderAddress = matchingSent.accountId.replace("external:", "");
796
+ return {
797
+ primaryType: "transfer",
798
+ direction: isObserverMode ? "neutral" : "incoming",
799
+ primaryAmount: received.amount,
800
+ secondaryAmount: null,
801
+ counterparty: {
802
+ type: "unknown",
803
+ address: senderAddress,
804
+ name: `${senderAddress.slice(0, 8)}...`
805
+ },
806
+ confidence: isObserverMode ? 0.9 : 0.95,
807
+ isRelevant: true,
808
+ metadata: {
809
+ ...isObserverMode && { observer_mode: true, receiver: participantAddress },
810
+ ...facilitator && { facilitator, payment_type: "facilitated" }
811
+ }
812
+ };
813
+ }
814
+ }
815
+ return null;
816
+ }
817
+ };
818
+
819
+ // ../classification/src/protocols/detector.ts
820
+ var KNOWN_PROGRAMS = {
821
+ [JUPITER_V6_PROGRAM_ID]: {
822
+ id: "jupiter",
823
+ name: "Jupiter"
824
+ },
825
+ [JUPITER_V4_PROGRAM_ID]: {
826
+ id: "jupiter-v4",
827
+ name: "Jupiter V4"
828
+ },
829
+ [TOKEN_PROGRAM_ID]: {
830
+ id: "spl-token",
831
+ name: "Token Program"
832
+ },
833
+ [SYSTEM_PROGRAM_ID]: {
834
+ id: "system",
835
+ name: "System Program"
836
+ },
837
+ [COMPUTE_BUDGET_PROGRAM_ID]: {
838
+ id: "compute-budget",
839
+ name: "Compute Budget"
840
+ },
841
+ [ASSOCIATED_TOKEN_PROGRAM_ID]: {
842
+ id: "associated-token",
843
+ name: "Associated Token Program"
844
+ },
845
+ [METAPLEX_PROGRAM_ID]: {
846
+ id: "metaplex",
847
+ name: "Metaplex"
848
+ },
849
+ [ORCA_WHIRLPOOL_PROGRAM_ID]: {
850
+ id: "orca-whirlpool",
851
+ name: "Orca Whirlpool"
852
+ },
853
+ [RAYDIUM_PROGRAM_ID]: {
854
+ id: "raydium",
855
+ name: "Raydium"
856
+ },
857
+ [STAKE_PROGRAM_ID]: {
858
+ id: "stake",
859
+ name: "Stake Program"
860
+ }
861
+ };
862
+ var PRIORITY_ORDER = [
863
+ "jupiter",
864
+ "jupiter-v4",
865
+ "raydium",
866
+ "orca-whirlpool",
867
+ "metaplex",
868
+ "stake",
869
+ "associated-token",
870
+ "spl-token",
871
+ "compute-budget",
872
+ "system"
873
+ ];
874
+ var DEX_PROTOCOL_IDS2 = /* @__PURE__ */ new Set([
875
+ "jupiter",
876
+ "jupiter-v4",
877
+ "raydium",
878
+ "orca-whirlpool"
879
+ ]);
880
+ function isDexProtocolById(protocolId) {
881
+ return protocolId !== void 0 && DEX_PROTOCOL_IDS2.has(protocolId);
882
+ }
883
+ function detectProtocol(programIds) {
884
+ const detectedProtocols = [];
885
+ for (const programId of programIds) {
886
+ const protocol = KNOWN_PROGRAMS[programId];
887
+ if (protocol) {
888
+ detectedProtocols.push(protocol);
889
+ }
890
+ }
891
+ if (detectedProtocols.length === 0) {
892
+ return null;
893
+ }
894
+ detectedProtocols.sort((a, b) => {
895
+ const aPriority = PRIORITY_ORDER.indexOf(a.id);
896
+ const bPriority = PRIORITY_ORDER.indexOf(b.id);
897
+ return aPriority - bPriority;
898
+ });
899
+ return detectedProtocols[0] ?? null;
900
+ }
901
+
902
+ // ../classification/src/classifiers/swap-classifier.ts
903
+ var SwapClassifier = class {
904
+ name = "swap";
905
+ priority = 80;
906
+ classify(context) {
907
+ const { legs, walletAddress, tx } = context;
908
+ const protocolLegs = legs.filter(
909
+ (leg) => leg.accountId.startsWith("protocol:")
910
+ );
911
+ const hasDexProtocol = isDexProtocolById(tx.protocol?.id);
912
+ if (protocolLegs.length === 0 && !hasDexProtocol) {
913
+ return null;
914
+ }
915
+ const isObserverMode = !walletAddress;
916
+ let participantPrefix;
917
+ if (walletAddress) {
918
+ participantPrefix = `wallet:${walletAddress}`;
919
+ } else {
920
+ const feeLeg = legs.find(
921
+ (leg) => leg.role === "fee" && leg.accountId.startsWith("external:")
922
+ );
923
+ if (!feeLeg) return null;
924
+ const feePayerAddress = feeLeg.accountId.replace("external:", "");
925
+ participantPrefix = `external:${feePayerAddress}`;
926
+ }
927
+ const participantDebits = legs.filter(
928
+ (leg) => leg.accountId.startsWith(participantPrefix) && leg.side === "debit"
929
+ );
930
+ const participantCredits = legs.filter(
931
+ (leg) => leg.accountId.startsWith(participantPrefix) && leg.side === "credit"
932
+ );
933
+ const tokensOut = participantDebits.filter(
934
+ (leg) => leg.role === "sent" || leg.role === "protocol_deposit"
935
+ );
936
+ const tokensIn = participantCredits.filter(
937
+ (leg) => leg.role === "received" || leg.role === "protocol_withdraw"
938
+ );
939
+ if (tokensOut.length === 0 || tokensIn.length === 0) {
940
+ return null;
941
+ }
942
+ if (tokensOut.length === 1 && tokensIn.length === 1) {
943
+ const tokenOut = tokensOut[0];
944
+ const tokenIn = tokensIn[0];
945
+ if (tokenOut.amount.token.symbol !== tokenIn.amount.token.symbol) {
946
+ return {
947
+ primaryType: "swap",
948
+ direction: "neutral",
949
+ primaryAmount: tokenOut.amount,
950
+ secondaryAmount: tokenIn.amount,
951
+ counterparty: null,
952
+ confidence: isObserverMode ? 0.85 : 0.9,
953
+ isRelevant: true,
954
+ metadata: {
955
+ swap_type: "token_to_token",
956
+ from_token: tokenOut.amount.token.symbol,
957
+ to_token: tokenIn.amount.token.symbol,
958
+ from_amount: tokenOut.amount.amountUi,
959
+ to_amount: tokenIn.amount.amountUi,
960
+ ...isObserverMode && { observer_mode: true }
961
+ }
962
+ };
963
+ }
964
+ }
965
+ return null;
966
+ }
967
+ };
968
+
969
+ // ../classification/src/classifiers/airdrop-classifier.ts
970
+ var AirdropClassifier = class {
971
+ name = "airdrop";
972
+ priority = 70;
973
+ classify(context) {
974
+ const { legs, walletAddress, tx } = context;
975
+ const facilitator = tx.accountKeys ? detectFacilitator(tx.accountKeys) : null;
976
+ const protocolLegs = legs.filter(
977
+ (leg) => leg.accountId.startsWith("protocol:")
978
+ );
979
+ if (protocolLegs.length === 0) {
980
+ return null;
981
+ }
982
+ const isObserverMode = !walletAddress;
983
+ const accountPrefix = walletAddress ? `wallet:${walletAddress}` : "external:";
984
+ const participantCredits = legs.filter(
985
+ (leg) => leg.accountId.startsWith(accountPrefix) && leg.side === "credit"
986
+ );
987
+ const participantDebits = legs.filter(
988
+ (leg) => leg.accountId.startsWith(accountPrefix) && leg.side === "debit"
989
+ );
990
+ const tokenReceived = participantCredits.filter(
991
+ (leg) => leg.role === "received" && leg.amount.token.symbol !== "SOL"
992
+ );
993
+ if (tokenReceived.length === 0) {
994
+ return null;
995
+ }
996
+ const tokenSent = participantDebits.filter(
997
+ (leg) => leg.role === "sent" && leg.amount.token.symbol !== "SOL"
998
+ );
999
+ if (tokenSent.length > 0) {
1000
+ return null;
1001
+ }
1002
+ const mainToken = tokenReceived[0];
1003
+ const senderLegs = legs.filter(
1004
+ (leg) => leg.side === "debit" && leg.amount.token.mint === mainToken.amount.token.mint && !leg.accountId.startsWith(accountPrefix)
1005
+ );
1006
+ const sender = senderLegs.length > 0 ? senderLegs[0] : null;
1007
+ const receiverAddress = mainToken.accountId.replace(/^(external:|wallet:)/, "");
1008
+ return {
1009
+ primaryType: "airdrop",
1010
+ direction: isObserverMode ? "neutral" : "incoming",
1011
+ primaryAmount: mainToken.amount,
1012
+ secondaryAmount: null,
1013
+ counterparty: sender ? {
1014
+ type: "unknown",
1015
+ address: sender.accountId.replace(/^(external:|protocol:|wallet:)/, "")
1016
+ } : null,
1017
+ confidence: isObserverMode ? 0.8 : 0.85,
1018
+ isRelevant: true,
1019
+ metadata: {
1020
+ airdrop_type: "token",
1021
+ token: mainToken.amount.token.symbol,
1022
+ amount: mainToken.amount.amountUi,
1023
+ ...isObserverMode && { observer_mode: true, receiver: receiverAddress },
1024
+ ...facilitator && { facilitator, payment_type: "facilitated" }
1025
+ }
1026
+ };
1027
+ }
1028
+ };
1029
+
1030
+ // ../classification/src/classifiers/fee-only-classifier.ts
1031
+ var FeeOnlyClassifier = class {
1032
+ name = "fee-only";
1033
+ priority = 60;
1034
+ classify(context) {
1035
+ const { legs, walletAddress } = context;
1036
+ const isObserverMode = !walletAddress;
1037
+ const accountPrefix = walletAddress ? `wallet:${walletAddress}` : "external:";
1038
+ const participantLegs = legs.filter((leg) => leg.accountId.startsWith(accountPrefix));
1039
+ const nonFeeParticipantLegs = participantLegs.filter((leg) => leg.role !== "fee");
1040
+ if (nonFeeParticipantLegs.length > 0) {
1041
+ return null;
1042
+ }
1043
+ const feeLegs = legs.filter((leg) => leg.role === "fee");
1044
+ if (feeLegs.length === 0) {
1045
+ return null;
1046
+ }
1047
+ const totalFee = feeLegs.find((leg) => leg.amount.token.symbol === "SOL");
1048
+ return {
1049
+ primaryType: "fee_only",
1050
+ direction: "outgoing",
1051
+ primaryAmount: totalFee?.amount ?? null,
1052
+ secondaryAmount: null,
1053
+ counterparty: null,
1054
+ confidence: isObserverMode ? 0.9 : 0.95,
1055
+ isRelevant: false,
1056
+ metadata: {
1057
+ fee_type: "network",
1058
+ ...isObserverMode && { observer_mode: true }
1059
+ }
1060
+ };
1061
+ }
1062
+ };
1063
+
1064
+ // ../classification/src/classifiers/solana-pay-classifier.ts
1065
+ var SolanaPayClassifier = class {
1066
+ name = "solana-pay";
1067
+ priority = 95;
1068
+ classify(context) {
1069
+ const { tx, walletAddress, legs } = context;
1070
+ if (!isSolanaPayTransaction(tx.programIds, tx.memo)) {
1071
+ return null;
1072
+ }
1073
+ const memo = parseSolanaPayMemo(tx.memo);
1074
+ const isObserverMode = !walletAddress;
1075
+ if (isObserverMode) {
1076
+ const senderLeg = legs.find(
1077
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role === "sent"
1078
+ );
1079
+ const receiverLeg = legs.find(
1080
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.role === "received"
1081
+ );
1082
+ const primaryAmount2 = senderLeg?.amount ?? receiverLeg?.amount ?? null;
1083
+ const senderAddress = senderLeg?.accountId.replace("external:", "");
1084
+ const receiverAddress = receiverLeg?.accountId.replace("external:", "");
1085
+ return {
1086
+ primaryType: "transfer",
1087
+ direction: "neutral",
1088
+ primaryAmount: primaryAmount2,
1089
+ secondaryAmount: null,
1090
+ counterparty: memo.merchant ? { address: receiverAddress ?? "", name: memo.merchant, type: "merchant" } : null,
1091
+ confidence: 0.95,
1092
+ isRelevant: true,
1093
+ metadata: {
1094
+ payment_type: "solana_pay",
1095
+ observer_mode: true,
1096
+ sender: senderAddress,
1097
+ receiver: receiverAddress,
1098
+ memo: memo.raw,
1099
+ merchant: memo.merchant,
1100
+ item: memo.item,
1101
+ reference: memo.reference,
1102
+ label: memo.label,
1103
+ message: memo.message
1104
+ }
1105
+ };
1106
+ }
1107
+ const walletPrefix = `wallet:${walletAddress}`;
1108
+ const userSent = legs.find(
1109
+ (leg) => leg.accountId.includes(walletPrefix) && leg.side === "debit" && leg.role === "sent"
1110
+ );
1111
+ const userReceived = legs.find(
1112
+ (leg) => leg.accountId.includes(walletPrefix) && leg.side === "credit" && leg.role === "received"
1113
+ );
1114
+ const direction = userSent ? "outgoing" : "incoming";
1115
+ const primaryAmount = userSent?.amount ?? userReceived?.amount ?? null;
1116
+ return {
1117
+ primaryType: "transfer",
1118
+ direction,
1119
+ primaryAmount,
1120
+ secondaryAmount: null,
1121
+ counterparty: memo.merchant ? { address: "", name: memo.merchant, type: "merchant" } : null,
1122
+ confidence: 0.98,
1123
+ isRelevant: true,
1124
+ metadata: {
1125
+ payment_type: "solana_pay",
1126
+ memo: memo.raw,
1127
+ merchant: memo.merchant,
1128
+ item: memo.item,
1129
+ reference: memo.reference,
1130
+ label: memo.label,
1131
+ message: memo.message
1132
+ }
1133
+ };
1134
+ }
1135
+ };
1136
+
1137
+ // ../classification/src/engine/classification-service.ts
1138
+ var ClassificationService = class {
1139
+ classifiers = [];
1140
+ constructor() {
1141
+ this.registerClassifier(new SolanaPayClassifier());
1142
+ this.registerClassifier(new SwapClassifier());
1143
+ this.registerClassifier(new AirdropClassifier());
1144
+ this.registerClassifier(new TransferClassifier());
1145
+ this.registerClassifier(new FeeOnlyClassifier());
1146
+ }
1147
+ registerClassifier(classifier) {
1148
+ this.classifiers.push(classifier);
1149
+ this.classifiers.sort((a, b) => b.priority - a.priority);
1150
+ }
1151
+ classify(legs, walletAddress, tx) {
1152
+ const context = { legs, walletAddress, tx };
1153
+ for (const classifier of this.classifiers) {
1154
+ const result = classifier.classify(context);
1155
+ if (result && result.isRelevant) {
1156
+ return result;
1157
+ }
1158
+ }
1159
+ return {
1160
+ primaryType: "other",
1161
+ direction: "neutral",
1162
+ primaryAmount: null,
1163
+ secondaryAmount: null,
1164
+ counterparty: null,
1165
+ confidence: 0,
1166
+ isRelevant: false,
1167
+ metadata: {}
1168
+ };
1169
+ }
1170
+ };
1171
+ var classificationService = new ClassificationService();
1172
+ function classifyTransaction(legs, walletAddress, tx) {
1173
+ return classificationService.classify(legs, walletAddress, tx);
1174
+ }
1175
+
1176
+ // ../domain/src/tx/spam-filter.ts
1177
+ var DEFAULT_CONFIG = {
1178
+ minSolAmount: 1e-3,
1179
+ minTokenAmountUsd: 0.01,
1180
+ minConfidence: 0.5,
1181
+ allowFailed: false
1182
+ };
1183
+ function isSpamTransaction(tx, classification, config = {}) {
1184
+ const cfg = { ...DEFAULT_CONFIG, ...config };
1185
+ if (!cfg.allowFailed && tx.err) {
1186
+ return true;
1187
+ }
1188
+ if (classification.confidence < cfg.minConfidence) {
1189
+ return true;
1190
+ }
1191
+ if (!classification.isRelevant) {
1192
+ return true;
1193
+ }
1194
+ if (isDustTransaction(classification, cfg)) {
1195
+ return true;
1196
+ }
1197
+ return false;
1198
+ }
1199
+ function isDustTransaction(classification, config) {
1200
+ const { primaryAmount } = classification;
1201
+ if (!primaryAmount) {
1202
+ return false;
1203
+ }
1204
+ const { token, amountUi } = primaryAmount;
1205
+ if (token.symbol === "SOL") {
1206
+ return Math.abs(amountUi) < config.minSolAmount;
1207
+ }
1208
+ if (token.symbol === "USDC") {
1209
+ return Math.abs(amountUi) < config.minTokenAmountUsd;
1210
+ }
1211
+ return Math.abs(amountUi) < config.minTokenAmountUsd;
1212
+ }
1213
+ function filterSpamTransactions(transactions, config) {
1214
+ return transactions.filter(
1215
+ ({ tx, classification }) => !isSpamTransaction(tx, classification, config)
1216
+ );
1217
+ }
1218
+
1219
+ // src/client.ts
1220
+ function createIndexer(options) {
1221
+ const client = "client" in options ? options.client : createSolanaClient(options.rpcUrl, options.wsUrl);
1222
+ return {
1223
+ rpc: client.rpc,
1224
+ async getBalance(walletAddress, tokenMints) {
1225
+ return fetchWalletBalance(client.rpc, walletAddress, tokenMints);
1226
+ },
1227
+ async getTransactions(walletAddress, options2 = {}) {
1228
+ const { limit = 10, before, until, filterSpam = true, spamConfig } = options2;
1229
+ const signatures = await fetchWalletSignatures(client.rpc, walletAddress, {
1230
+ limit,
1231
+ before,
1232
+ until
1233
+ });
1234
+ if (signatures.length === 0) {
1235
+ return [];
1236
+ }
1237
+ const signatureObjects = signatures.map(
1238
+ (sig) => parseSignature(sig.signature)
1239
+ );
1240
+ const transactions = await fetchTransactionsBatch(
1241
+ client.rpc,
1242
+ signatureObjects
1243
+ );
1244
+ const classified = transactions.map((tx) => {
1245
+ tx.protocol = detectProtocol(tx.programIds);
1246
+ const legs = transactionToLegs(tx, walletAddress);
1247
+ const classification = classifyTransaction(legs, walletAddress, tx);
1248
+ return { tx, classification, legs };
1249
+ });
1250
+ if (filterSpam) {
1251
+ return filterSpamTransactions(classified, spamConfig);
1252
+ }
1253
+ return classified;
1254
+ },
1255
+ async getTransaction(signature2, walletAddress) {
1256
+ const tx = await fetchTransaction(client.rpc, signature2);
1257
+ if (!tx) {
1258
+ return null;
1259
+ }
1260
+ tx.protocol = detectProtocol(tx.programIds);
1261
+ const legs = transactionToLegs(tx, walletAddress);
1262
+ const classification = classifyTransaction(legs, walletAddress, tx);
1263
+ return { tx, classification, legs };
1264
+ },
1265
+ async getRawTransaction(signature2) {
1266
+ return fetchTransaction(client.rpc, signature2);
1267
+ }
1268
+ };
1269
+ }
1270
+
1271
+ // ../domain/src/tx/leg-validation.ts
1272
+ function validateLegsBalance(legs) {
1273
+ const byToken = {};
1274
+ for (const leg of legs) {
1275
+ const token = leg.amount.token.symbol;
1276
+ if (!byToken[token]) {
1277
+ byToken[token] = { debits: 0, credits: 0 };
1278
+ }
1279
+ if (leg.side === "debit") {
1280
+ byToken[token].debits += leg.amount.amountUi;
1281
+ } else {
1282
+ byToken[token].credits += leg.amount.amountUi;
1283
+ }
1284
+ }
1285
+ const result = {};
1286
+ let isBalanced = true;
1287
+ for (const [token, amounts] of Object.entries(byToken)) {
1288
+ const diff = Math.abs(amounts.debits - amounts.credits);
1289
+ result[token] = { ...amounts, diff };
1290
+ if (diff > 1e-6) {
1291
+ isBalanced = false;
1292
+ }
1293
+ }
1294
+ return { isBalanced, byToken: result };
1295
+ }
1296
+ function groupLegsByAccount(legs) {
1297
+ const grouped = {};
1298
+ for (const leg of legs) {
1299
+ if (!grouped[leg.accountId]) {
1300
+ grouped[leg.accountId] = [];
1301
+ }
1302
+ grouped[leg.accountId].push(leg);
1303
+ }
1304
+ return grouped;
1305
+ }
1306
+ function groupLegsByToken(legs) {
1307
+ const grouped = {};
1308
+ for (const leg of legs) {
1309
+ const token = leg.amount.token.symbol;
1310
+ if (!grouped[token]) {
1311
+ grouped[token] = [];
1312
+ }
1313
+ grouped[token].push(leg);
1314
+ }
1315
+ return grouped;
1316
+ }
1317
+
1318
+ export { JUPITER_V4_PROGRAM_ID, JUPITER_V6_PROGRAM_ID, KNOWN_TOKENS, SPL_MEMO_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_INFO, TOKEN_PROGRAM_ID, buildAccountId, classifyTransaction, createIndexer, createSolanaClient, detectFacilitator, detectProtocol, extractMemo, fetchTransaction, fetchTransactionsBatch, fetchWalletBalance, fetchWalletSignatures, filterSpamTransactions, getTokenInfo, groupLegsByAccount, groupLegsByToken, isSolanaPayTransaction, isSpamTransaction, parseAccountId, parseAddress, parseSignature, parseSolanaPayMemo, transactionToLegs, validateLegsBalance };
1319
+ //# sourceMappingURL=index.js.map
1320
+ //# sourceMappingURL=index.js.map