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/README.md +81 -0
- package/dist/classification.types-w82k4B1F.d.ts +212 -0
- package/dist/client-BS9KUBU7.d.ts +257 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +1231 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +217 -0
- package/dist/index.js +1320 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +75 -0
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
|