tx-indexer 0.4.0 → 0.5.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 CHANGED
@@ -28,8 +28,42 @@ const txs = await indexer.getTransactions("YourWalletAddress...", {
28
28
  filterSpam: true
29
29
  });
30
30
 
31
- // Get single transaction
32
- const tx = await indexer.getTransaction("signature...", "walletAddress...");
31
+ // Get single transaction (no wallet required)
32
+ const tx = await indexer.getTransaction("signature...");
33
+
34
+ // Classification includes sender/receiver
35
+ console.log(tx.classification.primaryType); // "transfer", "swap", "nft_mint", etc.
36
+ console.log(tx.classification.sender); // sender address
37
+ console.log(tx.classification.receiver); // receiver address
38
+ ```
39
+
40
+ ## Transaction Types
41
+
42
+ - `transfer` - Wallet-to-wallet transfers
43
+ - `swap` - Token exchanges (pattern-based detection works with any DEX, higher confidence for known protocols like Jupiter, Raydium, Orca)
44
+ - `nft_mint` - NFT minting (Metaplex, Candy Machine, Bubblegum)
45
+ - `stake_deposit` - SOL staking deposits
46
+ - `stake_withdraw` - SOL staking withdrawals
47
+ - `bridge_in` - Receiving from bridge (Wormhole, deBridge, Allbridge)
48
+ - `bridge_out` - Sending to bridge
49
+ - `airdrop` - Token distributions
50
+ - `fee_only` - Transactions with only network fees
51
+
52
+ ## Frontend Integration
53
+
54
+ Classification is wallet-agnostic. Determine perspective in your frontend:
55
+
56
+ ```typescript
57
+ const { classification } = await indexer.getTransaction(signature);
58
+ const connectedWallet = wallet?.address;
59
+
60
+ if (connectedWallet === classification.sender) {
61
+ // "You sent..."
62
+ } else if (connectedWallet === classification.receiver) {
63
+ // "You received..."
64
+ } else {
65
+ // "Address X sent to Address Y"
66
+ }
33
67
  ```
34
68
 
35
69
  ## Bundle Size
@@ -78,4 +112,3 @@ bun run size:why
78
112
  ## License
79
113
 
80
114
  MIT
81
-
@@ -8,11 +8,11 @@ declare const TxDirectionSchema: z.ZodEnum<{
8
8
  neutral: "neutral";
9
9
  }>;
10
10
  declare const TxPrimaryTypeSchema: z.ZodEnum<{
11
- transfer: "transfer";
12
- swap: "swap";
11
+ nft_mint: "nft_mint";
13
12
  nft_purchase: "nft_purchase";
14
13
  nft_sale: "nft_sale";
15
- nft_mint: "nft_mint";
14
+ transfer: "transfer";
15
+ swap: "swap";
16
16
  stake_deposit: "stake_deposit";
17
17
  stake_withdraw: "stake_withdraw";
18
18
  token_deposit: "token_deposit";
@@ -50,6 +50,7 @@ declare const RawTransactionSchema: z.ZodObject<{
50
50
  signature: z.ZodCustom<Signature, Signature>;
51
51
  slot: z.ZodUnion<readonly [z.ZodNumber, z.ZodBigInt]>;
52
52
  blockTime: z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodBigInt]>>;
53
+ fee: z.ZodOptional<z.ZodNumber>;
53
54
  err: z.ZodNullable<z.ZodAny>;
54
55
  programIds: z.ZodArray<z.ZodString>;
55
56
  protocol: z.ZodNullable<z.ZodObject<{
@@ -91,11 +92,11 @@ declare const TxLegSideSchema: z.ZodEnum<{
91
92
  credit: "credit";
92
93
  }>;
93
94
  declare const TxLegRoleSchema: z.ZodEnum<{
94
- unknown: "unknown";
95
95
  reward: "reward";
96
+ unknown: "unknown";
97
+ fee: "fee";
96
98
  sent: "sent";
97
99
  received: "received";
98
- fee: "fee";
99
100
  protocol_deposit: "protocol_deposit";
100
101
  protocol_withdraw: "protocol_withdraw";
101
102
  principal: "principal";
@@ -143,11 +144,11 @@ declare const TxLegSchema: z.ZodObject<{
143
144
  } | undefined;
144
145
  }>;
145
146
  role: z.ZodEnum<{
146
- unknown: "unknown";
147
147
  reward: "reward";
148
+ unknown: "unknown";
149
+ fee: "fee";
148
150
  sent: "sent";
149
151
  received: "received";
150
- fee: "fee";
151
152
  protocol_deposit: "protocol_deposit";
152
153
  protocol_withdraw: "protocol_withdraw";
153
154
  principal: "principal";
@@ -165,11 +166,11 @@ type TxLeg = z.infer<typeof TxLegSchema>;
165
166
 
166
167
  declare const TransactionClassificationSchema: z.ZodObject<{
167
168
  primaryType: z.ZodEnum<{
168
- transfer: "transfer";
169
- swap: "swap";
169
+ nft_mint: "nft_mint";
170
170
  nft_purchase: "nft_purchase";
171
171
  nft_sale: "nft_sale";
172
- nft_mint: "nft_mint";
172
+ transfer: "transfer";
173
+ swap: "swap";
173
174
  stake_deposit: "stake_deposit";
174
175
  stake_withdraw: "stake_withdraw";
175
176
  token_deposit: "token_deposit";
@@ -187,12 +188,12 @@ declare const TransactionClassificationSchema: z.ZodObject<{
187
188
  receiver: z.ZodOptional<z.ZodNullable<z.ZodString>>;
188
189
  counterparty: z.ZodNullable<z.ZodObject<{
189
190
  type: z.ZodEnum<{
190
- unknown: "unknown";
191
- protocol: "protocol";
192
191
  person: "person";
193
192
  merchant: "merchant";
194
193
  exchange: "exchange";
194
+ protocol: "protocol";
195
195
  own_wallet: "own_wallet";
196
+ unknown: "unknown";
196
197
  }>;
197
198
  address: z.ZodString;
198
199
  name: z.ZodOptional<z.ZodString>;
@@ -1,5 +1,5 @@
1
1
  import { Rpc, GetBalanceApi, GetTokenAccountsByOwnerApi, GetSignaturesForAddressApi, GetTransactionApi, RpcSubscriptions, Address, Signature } from '@solana/kit';
2
- import { R as RawTransaction, T as TxLeg, a as TransactionClassification } from './classification.types-DlJe6bDZ.js';
2
+ import { R as RawTransaction, T as TxLeg, a as TransactionClassification } from './classification.types-Cn9IGtEC.js';
3
3
 
4
4
  /**
5
5
  * Union type of all RPC APIs used by the transaction indexer.
@@ -152,6 +152,36 @@ interface TokenAccountBalance {
152
152
  */
153
153
  declare function fetchWalletBalance(rpc: Rpc<GetBalanceApi & GetTokenAccountsByOwnerApi>, walletAddress: Address, tokenMints?: readonly string[]): Promise<WalletBalance>;
154
154
 
155
+ interface NftMetadata {
156
+ mint: string;
157
+ name: string;
158
+ symbol: string;
159
+ image: string;
160
+ cdnImage?: string;
161
+ description?: string;
162
+ collection?: string;
163
+ attributes?: Array<{
164
+ trait_type: string;
165
+ value: string;
166
+ }>;
167
+ }
168
+ /**
169
+ * Fetches NFT metadata from Helius DAS API.
170
+ *
171
+ * @param rpcUrl - Helius RPC endpoint URL
172
+ * @param mintAddress - NFT mint address
173
+ * @returns NFT metadata including name, image, collection, and attributes, or null if not found
174
+ */
175
+ declare function fetchNftMetadata(rpcUrl: string, mintAddress: string): Promise<NftMetadata | null>;
176
+ /**
177
+ * Fetches NFT metadata for multiple mints in parallel.
178
+ *
179
+ * @param rpcUrl - Helius RPC endpoint URL
180
+ * @param mintAddresses - Array of NFT mint addresses
181
+ * @returns Map of mint address to NFT metadata (only includes successful fetches)
182
+ */
183
+ declare function fetchNftMetadataBatch(rpcUrl: string, mintAddresses: string[]): Promise<Map<string, NftMetadata>>;
184
+
155
185
  type TxIndexerOptions = {
156
186
  rpcUrl: string;
157
187
  wsUrl?: string;
@@ -164,6 +194,10 @@ interface GetTransactionsOptions {
164
194
  until?: Signature;
165
195
  filterSpam?: boolean;
166
196
  spamConfig?: SpamFilterConfig;
197
+ enrichNftMetadata?: boolean;
198
+ }
199
+ interface GetTransactionOptions {
200
+ enrichNftMetadata?: boolean;
167
201
  }
168
202
  interface ClassifiedTransaction {
169
203
  tx: RawTransaction;
@@ -174,9 +208,11 @@ interface TxIndexer {
174
208
  rpc: ReturnType<typeof createSolanaClient>["rpc"];
175
209
  getBalance(walletAddress: Address, tokenMints?: readonly string[]): Promise<WalletBalance>;
176
210
  getTransactions(walletAddress: Address, options?: GetTransactionsOptions): Promise<ClassifiedTransaction[]>;
177
- getTransaction(signature: Signature): Promise<ClassifiedTransaction | null>;
211
+ getTransaction(signature: Signature, options?: GetTransactionOptions): Promise<ClassifiedTransaction | null>;
178
212
  getRawTransaction(signature: Signature): Promise<RawTransaction | null>;
213
+ getNftMetadata(mintAddress: string): Promise<NftMetadata | null>;
214
+ getNftMetadataBatch(mintAddresses: string[]): Promise<Map<string, NftMetadata>>;
179
215
  }
180
216
  declare function createIndexer(options: TxIndexerOptions): TxIndexer;
181
217
 
182
- export { type ClassifiedTransaction as C, type FetchTransactionsConfig as F, type GetTransactionsOptions as G, type IndexerRpcApi as I, type SolanaClient as S, type TxIndexer as T, type WalletBalance as W, type TxIndexerOptions as a, createSolanaClient as b, createIndexer as c, parseSignature as d, type TokenAccountBalance as e, fetchWalletBalance as f, fetchWalletSignatures as g, fetchTransaction as h, fetchTransactionsBatch as i, isSpamTransaction as j, filterSpamTransactions as k, type SpamFilterConfig as l, parseAddress as p, transactionToLegs as t };
218
+ export { type ClassifiedTransaction as C, type FetchTransactionsConfig as F, type GetTransactionsOptions as G, type IndexerRpcApi as I, type NftMetadata as N, type SolanaClient as S, type TxIndexer as T, type WalletBalance as W, type TxIndexerOptions as a, type GetTransactionOptions as b, createIndexer as c, createSolanaClient as d, parseSignature as e, fetchWalletBalance as f, type TokenAccountBalance as g, fetchWalletSignatures as h, fetchTransaction as i, fetchTransactionsBatch as j, isSpamTransaction as k, filterSpamTransactions as l, type SpamFilterConfig as m, fetchNftMetadata as n, fetchNftMetadataBatch as o, parseAddress as p, transactionToLegs as t };
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import '@solana/kit';
2
- export { C as ClassifiedTransaction, F as FetchTransactionsConfig, G as GetTransactionsOptions, T as TxIndexer, a as TxIndexerOptions, c as createIndexer } from './client-yGDWPKKf.js';
3
- import './classification.types-DlJe6bDZ.js';
2
+ export { C as ClassifiedTransaction, F as FetchTransactionsConfig, b as GetTransactionOptions, G as GetTransactionsOptions, T as TxIndexer, a as TxIndexerOptions, c as createIndexer } from './client-iLW2_DnL.js';
3
+ import './classification.types-Cn9IGtEC.js';
4
4
  import 'zod';
package/dist/client.js CHANGED
@@ -416,6 +416,7 @@ async function fetchTransaction(rpc, signature2, commitment = "confirmed") {
416
416
  signature: signature2,
417
417
  slot: response.slot,
418
418
  blockTime: response.blockTime,
419
+ fee: Number(response.meta?.fee ?? 0),
419
420
  err: response.meta?.err ?? null,
420
421
  programIds: extractProgramIds(response.transaction),
421
422
  protocol: null,
@@ -705,7 +706,7 @@ var TransferClassifier = class {
705
706
  sender,
706
707
  receiver,
707
708
  counterparty: {
708
- type: "wallet",
709
+ type: "unknown",
709
710
  address: receiver,
710
711
  name: `${receiver.slice(0, 8)}...`
711
712
  },
@@ -876,19 +877,19 @@ var SwapClassifier = class {
876
877
  priority = 80;
877
878
  classify(context) {
878
879
  const { legs, tx } = context;
879
- const hasDexProtocol = isDexProtocolById(tx.protocol?.id);
880
- if (!hasDexProtocol) {
881
- return null;
882
- }
883
880
  const feeLeg = legs.find(
884
881
  (leg) => leg.role === "fee" && leg.side === "debit"
885
882
  );
886
883
  const initiator = feeLeg?.accountId.replace("external:", "") ?? null;
884
+ if (!initiator) {
885
+ return null;
886
+ }
887
+ const initiatorAccountId = `external:${initiator}`;
887
888
  const tokensOut = legs.filter(
888
- (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && (leg.role === "sent" || leg.role === "protocol_deposit")
889
+ (leg) => leg.accountId === initiatorAccountId && leg.side === "debit" && (leg.role === "sent" || leg.role === "protocol_deposit")
889
890
  );
890
891
  const tokensIn = legs.filter(
891
- (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && (leg.role === "received" || leg.role === "protocol_withdraw")
892
+ (leg) => leg.accountId === initiatorAccountId && leg.side === "credit" && (leg.role === "received" || leg.role === "protocol_withdraw")
892
893
  );
893
894
  if (tokensOut.length === 0 || tokensIn.length === 0) {
894
895
  return null;
@@ -898,6 +899,8 @@ var SwapClassifier = class {
898
899
  if (tokenOut.amount.token.symbol === tokenIn.amount.token.symbol) {
899
900
  return null;
900
901
  }
902
+ const hasDexProtocol = isDexProtocolById(tx.protocol?.id);
903
+ const confidence = hasDexProtocol ? 0.95 : 0.75;
901
904
  return {
902
905
  primaryType: "swap",
903
906
  primaryAmount: tokenOut.amount,
@@ -905,7 +908,7 @@ var SwapClassifier = class {
905
908
  sender: initiator,
906
909
  receiver: initiator,
907
910
  counterparty: null,
908
- confidence: 0.9,
911
+ confidence,
909
912
  isRelevant: true,
910
913
  metadata: {
911
914
  swap_type: "token_to_token",
@@ -1037,7 +1040,7 @@ var SolanaPayClassifier = class {
1037
1040
  counterparty: receiver ? {
1038
1041
  address: receiver,
1039
1042
  name: memo.merchant ?? void 0,
1040
- type: memo.merchant ? "merchant" : "wallet"
1043
+ type: memo.merchant ? "merchant" : "unknown"
1041
1044
  } : null,
1042
1045
  confidence: 0.98,
1043
1046
  isRelevant: true,
@@ -1314,8 +1317,81 @@ function filterSpamTransactions(transactions, config) {
1314
1317
  );
1315
1318
  }
1316
1319
 
1320
+ // src/nft.ts
1321
+ async function fetchNftMetadata(rpcUrl, mintAddress) {
1322
+ const response = await fetch(rpcUrl, {
1323
+ method: "POST",
1324
+ headers: { "Content-Type": "application/json" },
1325
+ body: JSON.stringify({
1326
+ jsonrpc: "2.0",
1327
+ id: "get-asset",
1328
+ method: "getAsset",
1329
+ params: { id: mintAddress }
1330
+ })
1331
+ });
1332
+ const data = await response.json();
1333
+ if (data.error || !data.result?.content?.metadata) {
1334
+ return null;
1335
+ }
1336
+ const { result } = data;
1337
+ const { content, grouping } = result;
1338
+ return {
1339
+ mint: mintAddress,
1340
+ name: content.metadata.name,
1341
+ symbol: content.metadata.symbol,
1342
+ image: content.links.image ?? content.files?.[0]?.uri ?? "",
1343
+ cdnImage: content.files?.[0]?.cdn_uri,
1344
+ description: content.metadata.description,
1345
+ collection: grouping?.find((g) => g.group_key === "collection")?.group_value,
1346
+ attributes: content.metadata.attributes
1347
+ };
1348
+ }
1349
+ async function fetchNftMetadataBatch(rpcUrl, mintAddresses) {
1350
+ const results = await Promise.all(
1351
+ mintAddresses.map((mint) => fetchNftMetadata(rpcUrl, mint))
1352
+ );
1353
+ const map = /* @__PURE__ */ new Map();
1354
+ results.forEach((metadata, index) => {
1355
+ if (metadata) {
1356
+ map.set(mintAddresses[index], metadata);
1357
+ }
1358
+ });
1359
+ return map;
1360
+ }
1361
+
1317
1362
  // src/client.ts
1363
+ var NFT_TRANSACTION_TYPES = ["nft_mint", "nft_purchase", "nft_sale"];
1364
+ async function enrichNftClassification(rpcUrl, classified) {
1365
+ const { classification } = classified;
1366
+ if (!NFT_TRANSACTION_TYPES.includes(classification.primaryType)) {
1367
+ return classified;
1368
+ }
1369
+ const nftMint = classification.metadata?.nft_mint;
1370
+ if (!nftMint) {
1371
+ return classified;
1372
+ }
1373
+ const nftData = await fetchNftMetadata(rpcUrl, nftMint);
1374
+ if (!nftData) {
1375
+ return classified;
1376
+ }
1377
+ return {
1378
+ ...classified,
1379
+ classification: {
1380
+ ...classification,
1381
+ metadata: {
1382
+ ...classification.metadata,
1383
+ nft_name: nftData.name,
1384
+ nft_image: nftData.image,
1385
+ nft_cdn_image: nftData.cdnImage,
1386
+ nft_collection: nftData.collection,
1387
+ nft_symbol: nftData.symbol,
1388
+ nft_attributes: nftData.attributes
1389
+ }
1390
+ }
1391
+ };
1392
+ }
1318
1393
  function createIndexer(options) {
1394
+ const rpcUrl = "client" in options ? "" : options.rpcUrl;
1319
1395
  const client = "client" in options ? options.client : createSolanaClient(options.rpcUrl, options.wsUrl);
1320
1396
  return {
1321
1397
  rpc: client.rpc,
@@ -1323,7 +1399,39 @@ function createIndexer(options) {
1323
1399
  return fetchWalletBalance(client.rpc, walletAddress, tokenMints);
1324
1400
  },
1325
1401
  async getTransactions(walletAddress, options2 = {}) {
1326
- const { limit = 10, before, until, filterSpam = true, spamConfig } = options2;
1402
+ const { limit = 10, before, until, filterSpam = true, spamConfig, enrichNftMetadata = true } = options2;
1403
+ async function enrichBatch(transactions) {
1404
+ if (!enrichNftMetadata || !rpcUrl) {
1405
+ return transactions;
1406
+ }
1407
+ const nftMints = transactions.filter((t) => NFT_TRANSACTION_TYPES.includes(t.classification.primaryType)).map((t) => t.classification.metadata?.nft_mint).filter(Boolean);
1408
+ if (nftMints.length === 0) {
1409
+ return transactions;
1410
+ }
1411
+ const nftMetadataMap = await fetchNftMetadataBatch(rpcUrl, nftMints);
1412
+ return transactions.map((t) => {
1413
+ const nftMint = t.classification.metadata?.nft_mint;
1414
+ if (!nftMint || !nftMetadataMap.has(nftMint)) {
1415
+ return t;
1416
+ }
1417
+ const nftData = nftMetadataMap.get(nftMint);
1418
+ return {
1419
+ ...t,
1420
+ classification: {
1421
+ ...t.classification,
1422
+ metadata: {
1423
+ ...t.classification.metadata,
1424
+ nft_name: nftData.name,
1425
+ nft_image: nftData.image,
1426
+ nft_cdn_image: nftData.cdnImage,
1427
+ nft_collection: nftData.collection,
1428
+ nft_symbol: nftData.symbol,
1429
+ nft_attributes: nftData.attributes
1430
+ }
1431
+ }
1432
+ };
1433
+ });
1434
+ }
1327
1435
  if (!filterSpam) {
1328
1436
  const signatures = await fetchWalletSignatures(client.rpc, walletAddress, {
1329
1437
  limit,
@@ -1346,7 +1454,7 @@ function createIndexer(options) {
1346
1454
  const classification = classifyTransaction(legs, tx);
1347
1455
  return { tx, classification, legs };
1348
1456
  });
1349
- return classified;
1457
+ return enrichBatch(classified);
1350
1458
  }
1351
1459
  const accumulated = [];
1352
1460
  let currentBefore = before;
@@ -1385,9 +1493,11 @@ function createIndexer(options) {
1385
1493
  break;
1386
1494
  }
1387
1495
  }
1388
- return accumulated.slice(0, limit);
1496
+ const result = accumulated.slice(0, limit);
1497
+ return enrichBatch(result);
1389
1498
  },
1390
- async getTransaction(signature2) {
1499
+ async getTransaction(signature2, options2 = {}) {
1500
+ const { enrichNftMetadata = true } = options2;
1391
1501
  const tx = await fetchTransaction(client.rpc, signature2);
1392
1502
  if (!tx) {
1393
1503
  return null;
@@ -1395,10 +1505,26 @@ function createIndexer(options) {
1395
1505
  tx.protocol = detectProtocol(tx.programIds);
1396
1506
  const legs = transactionToLegs(tx);
1397
1507
  const classification = classifyTransaction(legs, tx);
1398
- return { tx, classification, legs };
1508
+ let classified = { tx, classification, legs };
1509
+ if (enrichNftMetadata && rpcUrl) {
1510
+ classified = await enrichNftClassification(rpcUrl, classified);
1511
+ }
1512
+ return classified;
1399
1513
  },
1400
1514
  async getRawTransaction(signature2) {
1401
1515
  return fetchTransaction(client.rpc, signature2);
1516
+ },
1517
+ async getNftMetadata(mintAddress) {
1518
+ if (!rpcUrl) {
1519
+ throw new Error("getNftMetadata requires rpcUrl to be set");
1520
+ }
1521
+ return fetchNftMetadata(rpcUrl, mintAddress);
1522
+ },
1523
+ async getNftMetadataBatch(mintAddresses) {
1524
+ if (!rpcUrl) {
1525
+ throw new Error("getNftMetadataBatch requires rpcUrl to be set");
1526
+ }
1527
+ return fetchNftMetadataBatch(rpcUrl, mintAddresses);
1402
1528
  }
1403
1529
  };
1404
1530
  }