tx-indexer 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -137,6 +137,7 @@ function extractProgramIds(transaction) {
137
137
  var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
138
138
  var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
139
139
  var STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111";
140
+ var STAKE_POOL_PROGRAM_ID = "SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy";
140
141
  var TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
141
142
  var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
142
143
  var SPL_MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
@@ -146,6 +147,15 @@ var JUPITER_V4_PROGRAM_ID = "JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB";
146
147
  var RAYDIUM_PROGRAM_ID = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
147
148
  var ORCA_WHIRLPOOL_PROGRAM_ID = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc";
148
149
  var METAPLEX_PROGRAM_ID = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
150
+ var CANDY_MACHINE_V3_PROGRAM_ID = "CndyV3LdqHUfDLmE5naZjVN8rBZz4tqhdefbAnjHG3JR";
151
+ var CANDY_GUARD_PROGRAM_ID = "Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g";
152
+ var BUBBLEGUM_PROGRAM_ID = "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY";
153
+ var MAGIC_EDEN_CANDY_MACHINE_ID = "CMZYPASGWeTz7RNGHaRJfCq2XQ5pYK6nDvVQxzkH51zb";
154
+ var WORMHOLE_PROGRAM_ID = "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth";
155
+ var WORMHOLE_TOKEN_BRIDGE_ID = "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb";
156
+ var DEGODS_BRIDGE_PROGRAM_ID = "35iLrpYNNR9ygHLcvE1xKFHbHq6paHthrF6wSovdWgGu";
157
+ var DEBRIDGE_PROGRAM_ID = "DEbrdGj3HsRsAzx6uH4MKyREKxVAfBydijLUF3ygsFfh";
158
+ var ALLBRIDGE_PROGRAM_ID = "BrdgN2RPzEMWF96ZbnnJaUtQDQx7VRXYaHHbYCBvceWB";
149
159
  var PAYAI_FACILITATOR = "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4";
150
160
  var KNOWN_FACILITATORS = [
151
161
  PAYAI_FACILITATOR
@@ -406,6 +416,7 @@ async function fetchTransaction(rpc, signature2, commitment = "confirmed") {
406
416
  signature: signature2,
407
417
  slot: response.slot,
408
418
  blockTime: response.blockTime,
419
+ fee: Number(response.meta?.fee ?? 0),
409
420
  err: response.meta?.err ?? null,
410
421
  programIds: extractProgramIds(response.transaction),
411
422
  protocol: null,
@@ -558,7 +569,7 @@ var DEX_PROTOCOL_IDS = /* @__PURE__ */ new Set([
558
569
  function isDexProtocol(protocol) {
559
570
  return protocol !== null && protocol !== void 0 && DEX_PROTOCOL_IDS.has(protocol.id);
560
571
  }
561
- function transactionToLegs(tx, walletAddress) {
572
+ function transactionToLegs(tx) {
562
573
  const legs = [];
563
574
  const feePayer = tx.accountKeys?.[0]?.toLowerCase();
564
575
  const solChanges = extractSolBalanceChanges(tx);
@@ -566,9 +577,8 @@ function transactionToLegs(tx, walletAddress) {
566
577
  let totalSolCredits = 0n;
567
578
  for (const change of solChanges) {
568
579
  if (change.change === 0n) continue;
569
- const isWallet = walletAddress ? change.address.toLowerCase() === walletAddress.toLowerCase() : false;
570
580
  const accountId = buildAccountId({
571
- type: isWallet ? "wallet" : "external",
581
+ type: "external",
572
582
  address: change.address
573
583
  });
574
584
  const solInfo = TOKEN_INFO[KNOWN_TOKENS.SOL];
@@ -588,7 +598,7 @@ function transactionToLegs(tx, walletAddress) {
588
598
  amountRaw: change.change.toString().replace("-", ""),
589
599
  amountUi: Math.abs(change.changeUi)
590
600
  },
591
- role: determineSolRole(change, walletAddress, tx, feePayer)
601
+ role: determineSolRole(change, tx, feePayer)
592
602
  });
593
603
  }
594
604
  const networkFee = totalSolDebits - totalSolCredits;
@@ -610,26 +620,22 @@ function transactionToLegs(tx, walletAddress) {
610
620
  const tokenChanges = extractTokenBalanceChanges(tx);
611
621
  for (const change of tokenChanges) {
612
622
  if (change.change.raw === "0") continue;
613
- const isWallet = walletAddress ? change.owner?.toLowerCase() === walletAddress.toLowerCase() : false;
614
- const isFeePayer = !walletAddress && feePayer ? change.owner?.toLowerCase() === feePayer : false;
615
623
  let accountId;
616
- if (isWallet && walletAddress) {
617
- accountId = buildAccountId({
618
- type: "wallet",
619
- address: change.owner || walletAddress
620
- });
621
- } else if (isFeePayer && feePayer) {
622
- accountId = buildAccountId({
623
- type: "external",
624
- address: change.owner || feePayer
625
- });
626
- } else if (isDexProtocol(tx.protocol) && walletAddress) {
627
- accountId = buildAccountId({
628
- type: "protocol",
629
- address: change.owner || change.tokenInfo.mint,
630
- protocol: tx.protocol.id,
631
- token: change.tokenInfo.symbol
632
- });
624
+ if (isDexProtocol(tx.protocol)) {
625
+ const isFeePayer = feePayer && change.owner?.toLowerCase() === feePayer;
626
+ if (isFeePayer) {
627
+ accountId = buildAccountId({
628
+ type: "external",
629
+ address: change.owner || feePayer
630
+ });
631
+ } else {
632
+ accountId = buildAccountId({
633
+ type: "protocol",
634
+ address: change.owner || change.tokenInfo.mint,
635
+ protocol: tx.protocol.id,
636
+ token: change.tokenInfo.symbol
637
+ });
638
+ }
633
639
  } else {
634
640
  accountId = buildAccountId({
635
641
  type: "external",
@@ -644,41 +650,30 @@ function transactionToLegs(tx, walletAddress) {
644
650
  amountRaw: change.change.raw.replace("-", ""),
645
651
  amountUi: Math.abs(change.change.ui)
646
652
  },
647
- role: determineTokenRole(change, walletAddress, tx, feePayer)
653
+ role: determineTokenRole(change, tx, feePayer)
648
654
  });
649
655
  }
650
656
  return legs;
651
657
  }
652
- function determineSolRole(change, walletAddress, tx, feePayer) {
653
- const isWallet = walletAddress ? change.address.toLowerCase() === walletAddress.toLowerCase() : false;
654
- const isFeePayer = !walletAddress && feePayer ? change.address.toLowerCase() === feePayer : false;
658
+ function determineSolRole(change, tx, feePayer) {
659
+ const isFeePayer = feePayer ? change.address.toLowerCase() === feePayer : false;
655
660
  const isPositive = change.change > 0n;
656
661
  const amountSol = Math.abs(change.changeUi);
657
- if (isWallet) {
658
- if (!isPositive && amountSol < 0.01) {
659
- return "fee";
660
- }
661
- if (isPositive) {
662
- if (tx.protocol?.id === "stake") {
663
- return "reward";
664
- }
665
- return "received";
666
- }
667
- return "sent";
668
- }
669
662
  if (isFeePayer && !isPositive && amountSol < 0.01) {
670
663
  return "fee";
671
664
  }
672
665
  if (isPositive) {
666
+ if (tx.protocol?.id === "stake") {
667
+ return "reward";
668
+ }
673
669
  return "received";
674
670
  }
675
671
  return "sent";
676
672
  }
677
- function determineTokenRole(change, walletAddress, tx, feePayer) {
678
- const isWallet = walletAddress ? change.owner?.toLowerCase() === walletAddress.toLowerCase() : false;
679
- const isFeePayer = !walletAddress && feePayer ? change.owner?.toLowerCase() === feePayer : false;
673
+ function determineTokenRole(change, tx, feePayer) {
674
+ const isFeePayer = feePayer ? change.owner?.toLowerCase() === feePayer : false;
680
675
  const isPositive = change.change.ui > 0;
681
- if (isWallet || isFeePayer) {
676
+ if (isFeePayer) {
682
677
  return isPositive ? "received" : "sent";
683
678
  }
684
679
  if (isDexProtocol(tx.protocol)) {
@@ -692,85 +687,35 @@ var TransferClassifier = class {
692
687
  name = "transfer";
693
688
  priority = 20;
694
689
  classify(context) {
695
- const { legs, walletAddress, tx } = context;
690
+ const { legs, tx } = context;
696
691
  const facilitator = tx.accountKeys ? detectFacilitator(tx.accountKeys) : null;
697
- const isObserverMode = !walletAddress;
698
- let participantAddress;
699
- let participantPrefix;
700
- if (walletAddress) {
701
- participantAddress = walletAddress.toLowerCase();
702
- participantPrefix = `wallet:${participantAddress}`;
703
- } else {
704
- const feeLeg = legs.find(
705
- (leg) => leg.role === "fee" && leg.accountId.startsWith("external:")
706
- );
707
- if (!feeLeg) return null;
708
- participantAddress = feeLeg.accountId.replace("external:", "").toLowerCase();
709
- participantPrefix = `external:${participantAddress}`;
710
- }
711
- const participantSent = legs.filter(
712
- (l) => l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.side === "debit" && l.role === "sent"
713
- );
714
- const participantReceived = legs.filter(
715
- (l) => l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.side === "credit" && l.role === "received"
716
- );
717
- const otherReceived = legs.filter(
718
- (l) => !l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.accountId.startsWith("external:") && l.side === "credit" && l.role === "received"
692
+ const senderLeg = legs.find(
693
+ (l) => l.accountId.startsWith("external:") && l.side === "debit" && l.role === "sent"
719
694
  );
720
- const otherSent = legs.filter(
721
- (l) => !l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.accountId.startsWith("external:") && l.side === "debit" && l.role === "sent"
695
+ if (!senderLeg) return null;
696
+ const sender = senderLeg.accountId.replace("external:", "");
697
+ const receiverLeg = legs.find(
698
+ (l) => l.accountId.startsWith("external:") && l.side === "credit" && l.role === "received" && l.amount.token.mint === senderLeg.amount.token.mint
722
699
  );
723
- for (const sent of participantSent) {
724
- const matchingReceived = otherReceived.find(
725
- (r) => r.amount.token.mint === sent.amount.token.mint
726
- );
727
- if (matchingReceived) {
728
- const receiverAddress = matchingReceived.accountId.replace("external:", "");
729
- return {
730
- primaryType: "transfer",
731
- direction: isObserverMode ? "neutral" : "outgoing",
732
- primaryAmount: sent.amount,
733
- secondaryAmount: null,
734
- counterparty: {
735
- type: "unknown",
736
- address: receiverAddress,
737
- name: `${receiverAddress.slice(0, 8)}...`
738
- },
739
- confidence: isObserverMode ? 0.9 : 0.95,
740
- isRelevant: true,
741
- metadata: {
742
- ...isObserverMode && { observer_mode: true, sender: participantAddress },
743
- ...facilitator && { facilitator, payment_type: "facilitated" }
744
- }
745
- };
746
- }
747
- }
748
- for (const received of participantReceived) {
749
- const matchingSent = otherSent.find(
750
- (s) => s.amount.token.mint === received.amount.token.mint
751
- );
752
- if (matchingSent) {
753
- const senderAddress = matchingSent.accountId.replace("external:", "");
754
- return {
755
- primaryType: "transfer",
756
- direction: isObserverMode ? "neutral" : "incoming",
757
- primaryAmount: received.amount,
758
- secondaryAmount: null,
759
- counterparty: {
760
- type: "unknown",
761
- address: senderAddress,
762
- name: `${senderAddress.slice(0, 8)}...`
763
- },
764
- confidence: isObserverMode ? 0.9 : 0.95,
765
- isRelevant: true,
766
- metadata: {
767
- ...isObserverMode && { observer_mode: true, receiver: participantAddress },
768
- ...facilitator && { facilitator, payment_type: "facilitated" }
769
- }
770
- };
700
+ if (!receiverLeg) return null;
701
+ const receiver = receiverLeg.accountId.replace("external:", "");
702
+ return {
703
+ primaryType: "transfer",
704
+ primaryAmount: senderLeg.amount,
705
+ secondaryAmount: null,
706
+ sender,
707
+ receiver,
708
+ counterparty: {
709
+ type: "wallet",
710
+ address: receiver,
711
+ name: `${receiver.slice(0, 8)}...`
712
+ },
713
+ confidence: 0.95,
714
+ isRelevant: true,
715
+ metadata: {
716
+ ...facilitator && { facilitator, payment_type: "facilitated" }
771
717
  }
772
- }
773
- return null;
718
+ };
774
719
  }
775
720
  };
776
721
 
@@ -815,9 +760,54 @@ var KNOWN_PROGRAMS = {
815
760
  [STAKE_PROGRAM_ID]: {
816
761
  id: "stake",
817
762
  name: "Stake Program"
763
+ },
764
+ [STAKE_POOL_PROGRAM_ID]: {
765
+ id: "stake-pool",
766
+ name: "Stake Pool Program"
767
+ },
768
+ [CANDY_GUARD_PROGRAM_ID]: {
769
+ id: "candy-guard",
770
+ name: "Metaplex Candy Guard Program"
771
+ },
772
+ [CANDY_MACHINE_V3_PROGRAM_ID]: {
773
+ id: "candy-machine-v3",
774
+ name: "Metaplex Candy Machine Core Program"
775
+ },
776
+ [BUBBLEGUM_PROGRAM_ID]: {
777
+ id: "bubblegum",
778
+ name: "Bubblegum Program"
779
+ },
780
+ [MAGIC_EDEN_CANDY_MACHINE_ID]: {
781
+ id: "magic-eden-candy-machine",
782
+ name: "Nft Candy Machine Program (Magic Eden)"
783
+ },
784
+ [WORMHOLE_PROGRAM_ID]: {
785
+ id: "wormhole",
786
+ name: "Wormhole"
787
+ },
788
+ [WORMHOLE_TOKEN_BRIDGE_ID]: {
789
+ id: "wormhole-token-bridge",
790
+ name: "Wormhole Token Bridge"
791
+ },
792
+ [DEGODS_BRIDGE_PROGRAM_ID]: {
793
+ id: "degods-bridge",
794
+ name: "DeGods Bridge"
795
+ },
796
+ [DEBRIDGE_PROGRAM_ID]: {
797
+ id: "debridge",
798
+ name: "deBridge"
799
+ },
800
+ [ALLBRIDGE_PROGRAM_ID]: {
801
+ id: "allbridge",
802
+ name: "Allbridge"
818
803
  }
819
804
  };
820
805
  var PRIORITY_ORDER = [
806
+ "wormhole",
807
+ "wormhole-token-bridge",
808
+ "degods-bridge",
809
+ "debridge",
810
+ "allbridge",
821
811
  "jupiter",
822
812
  "jupiter-v4",
823
813
  "raydium",
@@ -835,9 +825,33 @@ var DEX_PROTOCOL_IDS2 = /* @__PURE__ */ new Set([
835
825
  "raydium",
836
826
  "orca-whirlpool"
837
827
  ]);
828
+ var NFT_MINT_PROTOCOL_IDS = /* @__PURE__ */ new Set([
829
+ "metaplex",
830
+ "candy-machine-v3",
831
+ "candy-guard",
832
+ "bubblegum",
833
+ "magic-eden-candy-machine"
834
+ ]);
835
+ var STAKE_PROTOCOL_IDS = /* @__PURE__ */ new Set(["stake", "stake-pool"]);
836
+ var BRIDGE_PROTOCOL_IDS = /* @__PURE__ */ new Set([
837
+ "wormhole",
838
+ "wormhole-token-bridge",
839
+ "degods-bridge",
840
+ "debridge",
841
+ "allbridge"
842
+ ]);
838
843
  function isDexProtocolById(protocolId) {
839
844
  return protocolId !== void 0 && DEX_PROTOCOL_IDS2.has(protocolId);
840
845
  }
846
+ function isNftMintProtocolById(protocolId) {
847
+ return protocolId !== void 0 && NFT_MINT_PROTOCOL_IDS.has(protocolId);
848
+ }
849
+ function isStakeProtocolById(protocolId) {
850
+ return protocolId !== void 0 && STAKE_PROTOCOL_IDS.has(protocolId);
851
+ }
852
+ function isBridgeProtocolById(protocolId) {
853
+ return protocolId !== void 0 && BRIDGE_PROTOCOL_IDS.has(protocolId);
854
+ }
841
855
  function detectProtocol(programIds) {
842
856
  const detectedProtocols = [];
843
857
  for (const programId of programIds) {
@@ -862,65 +876,46 @@ var SwapClassifier = class {
862
876
  name = "swap";
863
877
  priority = 80;
864
878
  classify(context) {
865
- const { legs, walletAddress, tx } = context;
866
- const protocolLegs = legs.filter(
867
- (leg) => leg.accountId.startsWith("protocol:")
868
- );
879
+ const { legs, tx } = context;
869
880
  const hasDexProtocol = isDexProtocolById(tx.protocol?.id);
870
- if (protocolLegs.length === 0 && !hasDexProtocol) {
881
+ if (!hasDexProtocol) {
871
882
  return null;
872
883
  }
873
- const isObserverMode = !walletAddress;
874
- let participantPrefix;
875
- if (walletAddress) {
876
- participantPrefix = `wallet:${walletAddress}`;
877
- } else {
878
- const feeLeg = legs.find(
879
- (leg) => leg.role === "fee" && leg.accountId.startsWith("external:")
880
- );
881
- if (!feeLeg) return null;
882
- const feePayerAddress = feeLeg.accountId.replace("external:", "");
883
- participantPrefix = `external:${feePayerAddress}`;
884
- }
885
- const participantDebits = legs.filter(
886
- (leg) => leg.accountId.startsWith(participantPrefix) && leg.side === "debit"
887
- );
888
- const participantCredits = legs.filter(
889
- (leg) => leg.accountId.startsWith(participantPrefix) && leg.side === "credit"
884
+ const feeLeg = legs.find(
885
+ (leg) => leg.role === "fee" && leg.side === "debit"
890
886
  );
891
- const tokensOut = participantDebits.filter(
892
- (leg) => leg.role === "sent" || leg.role === "protocol_deposit"
887
+ const initiator = feeLeg?.accountId.replace("external:", "") ?? null;
888
+ const tokensOut = legs.filter(
889
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && (leg.role === "sent" || leg.role === "protocol_deposit")
893
890
  );
894
- const tokensIn = participantCredits.filter(
895
- (leg) => leg.role === "received" || leg.role === "protocol_withdraw"
891
+ const tokensIn = legs.filter(
892
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && (leg.role === "received" || leg.role === "protocol_withdraw")
896
893
  );
897
894
  if (tokensOut.length === 0 || tokensIn.length === 0) {
898
895
  return null;
899
896
  }
900
- if (tokensOut.length === 1 && tokensIn.length === 1) {
901
- const tokenOut = tokensOut[0];
902
- const tokenIn = tokensIn[0];
903
- if (tokenOut.amount.token.symbol !== tokenIn.amount.token.symbol) {
904
- return {
905
- primaryType: "swap",
906
- direction: "neutral",
907
- primaryAmount: tokenOut.amount,
908
- secondaryAmount: tokenIn.amount,
909
- counterparty: null,
910
- confidence: isObserverMode ? 0.85 : 0.9,
911
- isRelevant: true,
912
- metadata: {
913
- swap_type: "token_to_token",
914
- from_token: tokenOut.amount.token.symbol,
915
- to_token: tokenIn.amount.token.symbol,
916
- from_amount: tokenOut.amount.amountUi,
917
- to_amount: tokenIn.amount.amountUi,
918
- ...isObserverMode && { observer_mode: true }
919
- }
920
- };
921
- }
897
+ const tokenOut = tokensOut[0];
898
+ const tokenIn = tokensIn[0];
899
+ if (tokenOut.amount.token.symbol === tokenIn.amount.token.symbol) {
900
+ return null;
922
901
  }
923
- return null;
902
+ return {
903
+ primaryType: "swap",
904
+ primaryAmount: tokenOut.amount,
905
+ secondaryAmount: tokenIn.amount,
906
+ sender: initiator,
907
+ receiver: initiator,
908
+ counterparty: null,
909
+ confidence: 0.9,
910
+ isRelevant: true,
911
+ metadata: {
912
+ swap_type: "token_to_token",
913
+ from_token: tokenOut.amount.token.symbol,
914
+ to_token: tokenIn.amount.token.symbol,
915
+ from_amount: tokenOut.amount.amountUi,
916
+ to_amount: tokenIn.amount.amountUi
917
+ }
918
+ };
924
919
  }
925
920
  };
926
921
 
@@ -929,7 +924,7 @@ var AirdropClassifier = class {
929
924
  name = "airdrop";
930
925
  priority = 70;
931
926
  classify(context) {
932
- const { legs, walletAddress, tx } = context;
927
+ const { legs, tx } = context;
933
928
  const facilitator = tx.accountKeys ? detectFacilitator(tx.accountKeys) : null;
934
929
  const protocolLegs = legs.filter(
935
930
  (leg) => leg.accountId.startsWith("protocol:")
@@ -937,49 +932,41 @@ var AirdropClassifier = class {
937
932
  if (protocolLegs.length === 0) {
938
933
  return null;
939
934
  }
940
- const isObserverMode = !walletAddress;
941
- const accountPrefix = walletAddress ? `wallet:${walletAddress}` : "external:";
942
- const participantCredits = legs.filter(
943
- (leg) => leg.accountId.startsWith(accountPrefix) && leg.side === "credit"
944
- );
945
- const participantDebits = legs.filter(
946
- (leg) => leg.accountId.startsWith(accountPrefix) && leg.side === "debit"
947
- );
948
- const tokenReceived = participantCredits.filter(
949
- (leg) => leg.role === "received" && leg.amount.token.symbol !== "SOL"
935
+ const tokenReceived = legs.filter(
936
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.role === "received" && leg.amount.token.symbol !== "SOL"
950
937
  );
951
938
  if (tokenReceived.length === 0) {
952
939
  return null;
953
940
  }
954
- const tokenSent = participantDebits.filter(
955
- (leg) => leg.role === "sent" && leg.amount.token.symbol !== "SOL"
941
+ const tokenSent = legs.filter(
942
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role === "sent" && leg.amount.token.symbol !== "SOL"
956
943
  );
957
944
  if (tokenSent.length > 0) {
958
945
  return null;
959
946
  }
960
947
  const mainToken = tokenReceived[0];
961
- const senderLegs = legs.filter(
962
- (leg) => leg.side === "debit" && leg.amount.token.mint === mainToken.amount.token.mint && !leg.accountId.startsWith(accountPrefix)
948
+ const receiver = mainToken.accountId.replace("external:", "");
949
+ const senderLeg = legs.find(
950
+ (leg) => leg.side === "debit" && leg.amount.token.mint === mainToken.amount.token.mint
963
951
  );
964
- const sender = senderLegs.length > 0 ? senderLegs[0] : null;
965
- const receiverAddress = mainToken.accountId.replace(/^(external:|wallet:)/, "");
952
+ const sender = senderLeg ? senderLeg.accountId.replace(/^(external:|protocol:)/, "") : null;
966
953
  return {
967
954
  primaryType: "airdrop",
968
- direction: isObserverMode ? "neutral" : "incoming",
969
955
  primaryAmount: mainToken.amount,
970
956
  secondaryAmount: null,
957
+ sender,
958
+ receiver,
971
959
  counterparty: sender ? {
972
- type: "unknown",
973
- address: sender.accountId.replace(/^(external:|protocol:|wallet:)/, "")
960
+ type: "protocol",
961
+ address: sender
974
962
  } : null,
975
- confidence: isObserverMode ? 0.8 : 0.85,
963
+ confidence: 0.85,
976
964
  isRelevant: true,
977
965
  metadata: {
978
966
  airdrop_type: "token",
979
967
  token: mainToken.amount.token.symbol,
980
968
  amount: mainToken.amount.amountUi,
981
- ...isObserverMode && { observer_mode: true, receiver: receiverAddress },
982
- ...facilitator && { facilitator, payment_type: "facilitated" }
969
+ ...facilitator && { facilitator }
983
970
  }
984
971
  };
985
972
  }
@@ -990,30 +977,34 @@ var FeeOnlyClassifier = class {
990
977
  name = "fee-only";
991
978
  priority = 60;
992
979
  classify(context) {
993
- const { legs, walletAddress } = context;
994
- const isObserverMode = !walletAddress;
995
- const accountPrefix = walletAddress ? `wallet:${walletAddress}` : "external:";
996
- const participantLegs = legs.filter((leg) => leg.accountId.startsWith(accountPrefix));
997
- const nonFeeParticipantLegs = participantLegs.filter((leg) => leg.role !== "fee");
998
- if (nonFeeParticipantLegs.length > 0) {
980
+ const { legs } = context;
981
+ const externalLegs = legs.filter(
982
+ (leg) => leg.accountId.startsWith("external:")
983
+ );
984
+ const nonFeeLegs = externalLegs.filter((leg) => leg.role !== "fee");
985
+ if (nonFeeLegs.length > 0) {
999
986
  return null;
1000
987
  }
1001
988
  const feeLegs = legs.filter((leg) => leg.role === "fee");
1002
989
  if (feeLegs.length === 0) {
1003
990
  return null;
1004
991
  }
992
+ const feePayerLeg = feeLegs.find(
993
+ (leg) => leg.side === "debit" && leg.amount.token.symbol === "SOL"
994
+ );
995
+ const feePayer = feePayerLeg?.accountId.replace("external:", "") ?? null;
1005
996
  const totalFee = feeLegs.find((leg) => leg.amount.token.symbol === "SOL");
1006
997
  return {
1007
998
  primaryType: "fee_only",
1008
- direction: "outgoing",
1009
999
  primaryAmount: totalFee?.amount ?? null,
1010
1000
  secondaryAmount: null,
1001
+ sender: feePayer,
1002
+ receiver: null,
1011
1003
  counterparty: null,
1012
- confidence: isObserverMode ? 0.9 : 0.95,
1004
+ confidence: 0.95,
1013
1005
  isRelevant: false,
1014
1006
  metadata: {
1015
- fee_type: "network",
1016
- ...isObserverMode && { observer_mode: true }
1007
+ fee_type: "network"
1017
1008
  }
1018
1009
  };
1019
1010
  }
@@ -1024,59 +1015,31 @@ var SolanaPayClassifier = class {
1024
1015
  name = "solana-pay";
1025
1016
  priority = 95;
1026
1017
  classify(context) {
1027
- const { tx, walletAddress, legs } = context;
1018
+ const { tx, legs } = context;
1028
1019
  if (!isSolanaPayTransaction(tx.programIds, tx.memo)) {
1029
1020
  return null;
1030
1021
  }
1031
1022
  const memo = parseSolanaPayMemo(tx.memo);
1032
- const isObserverMode = !walletAddress;
1033
- if (isObserverMode) {
1034
- const senderLeg = legs.find(
1035
- (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role === "sent"
1036
- );
1037
- const receiverLeg = legs.find(
1038
- (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.role === "received"
1039
- );
1040
- const primaryAmount2 = senderLeg?.amount ?? receiverLeg?.amount ?? null;
1041
- const senderAddress = senderLeg?.accountId.replace("external:", "");
1042
- const receiverAddress = receiverLeg?.accountId.replace("external:", "");
1043
- return {
1044
- primaryType: "transfer",
1045
- direction: "neutral",
1046
- primaryAmount: primaryAmount2,
1047
- secondaryAmount: null,
1048
- counterparty: memo.merchant ? { address: receiverAddress ?? "", name: memo.merchant, type: "merchant" } : null,
1049
- confidence: 0.95,
1050
- isRelevant: true,
1051
- metadata: {
1052
- payment_type: "solana_pay",
1053
- observer_mode: true,
1054
- sender: senderAddress,
1055
- receiver: receiverAddress,
1056
- memo: memo.raw,
1057
- merchant: memo.merchant,
1058
- item: memo.item,
1059
- reference: memo.reference,
1060
- label: memo.label,
1061
- message: memo.message
1062
- }
1063
- };
1064
- }
1065
- const walletPrefix = `wallet:${walletAddress}`;
1066
- const userSent = legs.find(
1067
- (leg) => leg.accountId.includes(walletPrefix) && leg.side === "debit" && leg.role === "sent"
1023
+ const senderLeg = legs.find(
1024
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role === "sent"
1068
1025
  );
1069
- const userReceived = legs.find(
1070
- (leg) => leg.accountId.includes(walletPrefix) && leg.side === "credit" && leg.role === "received"
1026
+ const receiverLeg = legs.find(
1027
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.role === "received"
1071
1028
  );
1072
- const direction = userSent ? "outgoing" : "incoming";
1073
- const primaryAmount = userSent?.amount ?? userReceived?.amount ?? null;
1029
+ const sender = senderLeg?.accountId.replace("external:", "") ?? null;
1030
+ const receiver = receiverLeg?.accountId.replace("external:", "") ?? null;
1031
+ const primaryAmount = senderLeg?.amount ?? receiverLeg?.amount ?? null;
1074
1032
  return {
1075
1033
  primaryType: "transfer",
1076
- direction,
1077
1034
  primaryAmount,
1078
1035
  secondaryAmount: null,
1079
- counterparty: memo.merchant ? { address: "", name: memo.merchant, type: "merchant" } : null,
1036
+ sender,
1037
+ receiver,
1038
+ counterparty: receiver ? {
1039
+ address: receiver,
1040
+ name: memo.merchant ?? void 0,
1041
+ type: memo.merchant ? "merchant" : "wallet"
1042
+ } : null,
1080
1043
  confidence: 0.98,
1081
1044
  isRelevant: true,
1082
1045
  metadata: {
@@ -1092,11 +1055,188 @@ var SolanaPayClassifier = class {
1092
1055
  }
1093
1056
  };
1094
1057
 
1058
+ // ../classification/src/classifiers/nft-mint-classifier.ts
1059
+ var NftMintClassifier = class {
1060
+ name = "nft-mint";
1061
+ priority = 85;
1062
+ classify(context) {
1063
+ const { legs, tx } = context;
1064
+ const hasNftMintProtocol = isNftMintProtocolById(tx.protocol?.id);
1065
+ if (!hasNftMintProtocol) {
1066
+ return null;
1067
+ }
1068
+ const nftCredits = legs.filter(
1069
+ (leg) => leg.side === "credit" && leg.amount.token.decimals === 0 && leg.amount.amountUi >= 1 && (leg.role === "received" || leg.role === "protocol_withdraw")
1070
+ );
1071
+ if (nftCredits.length === 0) {
1072
+ return null;
1073
+ }
1074
+ const primaryNft = nftCredits[0];
1075
+ const minter = primaryNft.accountId.replace("external:", "");
1076
+ const paymentLeg = legs.find(
1077
+ (leg) => leg.side === "debit" && leg.role === "sent" && leg.amount.token.symbol === "SOL"
1078
+ );
1079
+ const totalMinted = nftCredits.reduce(
1080
+ (sum, leg) => sum + leg.amount.amountUi,
1081
+ 0
1082
+ );
1083
+ return {
1084
+ primaryType: "nft_mint",
1085
+ primaryAmount: primaryNft.amount,
1086
+ secondaryAmount: paymentLeg?.amount ?? null,
1087
+ sender: null,
1088
+ receiver: minter,
1089
+ counterparty: null,
1090
+ confidence: 0.9,
1091
+ isRelevant: true,
1092
+ metadata: {
1093
+ nft_mint: primaryNft.amount.token.mint,
1094
+ nft_name: primaryNft.amount.token.name,
1095
+ quantity: totalMinted,
1096
+ mint_price: paymentLeg?.amount.amountUi,
1097
+ protocol: tx.protocol?.id
1098
+ }
1099
+ };
1100
+ }
1101
+ };
1102
+
1103
+ // ../classification/src/classifiers/stake-deposit-classifier.ts
1104
+ var StakeDepositClassifier = class {
1105
+ name = "stake-deposit";
1106
+ priority = 82;
1107
+ classify(context) {
1108
+ const { legs, tx } = context;
1109
+ const hasStakeProtocol = isStakeProtocolById(tx.protocol?.id);
1110
+ if (!hasStakeProtocol) {
1111
+ return null;
1112
+ }
1113
+ const solDebit = legs.find(
1114
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.amount.token.symbol === "SOL" && (leg.role === "sent" || leg.role === "protocol_deposit")
1115
+ );
1116
+ if (!solDebit) {
1117
+ return null;
1118
+ }
1119
+ const staker = solDebit.accountId.replace("external:", "");
1120
+ return {
1121
+ primaryType: "stake_deposit",
1122
+ primaryAmount: solDebit.amount,
1123
+ secondaryAmount: null,
1124
+ sender: staker,
1125
+ receiver: null,
1126
+ counterparty: null,
1127
+ confidence: 0.9,
1128
+ isRelevant: true,
1129
+ metadata: {
1130
+ stake_amount: solDebit.amount.amountUi,
1131
+ protocol: tx.protocol?.id
1132
+ }
1133
+ };
1134
+ }
1135
+ };
1136
+
1137
+ // ../classification/src/classifiers/stake-withdraw-classifier.ts
1138
+ var StakeWithdrawClassifier = class {
1139
+ name = "stake-withdraw";
1140
+ priority = 81;
1141
+ classify(context) {
1142
+ const { legs, tx } = context;
1143
+ const hasStakeProtocol = isStakeProtocolById(tx.protocol?.id);
1144
+ if (!hasStakeProtocol) {
1145
+ return null;
1146
+ }
1147
+ const solCredit = legs.find(
1148
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.amount.token.symbol === "SOL" && (leg.role === "received" || leg.role === "protocol_withdraw")
1149
+ );
1150
+ if (!solCredit) {
1151
+ return null;
1152
+ }
1153
+ const solDebit = legs.find(
1154
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.amount.token.symbol === "SOL" && (leg.role === "sent" || leg.role === "protocol_deposit")
1155
+ );
1156
+ if (solDebit) {
1157
+ return null;
1158
+ }
1159
+ const withdrawer = solCredit.accountId.replace("external:", "");
1160
+ return {
1161
+ primaryType: "stake_withdraw",
1162
+ primaryAmount: solCredit.amount,
1163
+ secondaryAmount: null,
1164
+ sender: null,
1165
+ receiver: withdrawer,
1166
+ counterparty: null,
1167
+ confidence: 0.9,
1168
+ isRelevant: true,
1169
+ metadata: {
1170
+ withdraw_amount: solCredit.amount.amountUi,
1171
+ protocol: tx.protocol?.id
1172
+ }
1173
+ };
1174
+ }
1175
+ };
1176
+
1177
+ // ../classification/src/classifiers/bridge-classifier.ts
1178
+ var BridgeClassifier = class {
1179
+ name = "bridge";
1180
+ priority = 88;
1181
+ classify(context) {
1182
+ const { legs, tx } = context;
1183
+ const hasBridgeProtocol = isBridgeProtocolById(tx.protocol?.id);
1184
+ if (!hasBridgeProtocol) {
1185
+ return null;
1186
+ }
1187
+ const tokensOut = legs.filter(
1188
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role !== "fee" && (leg.role === "sent" || leg.role === "protocol_deposit")
1189
+ );
1190
+ const tokensIn = legs.filter(
1191
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && (leg.role === "received" || leg.role === "protocol_withdraw")
1192
+ );
1193
+ let primaryType;
1194
+ let primaryAmount = null;
1195
+ let participant = null;
1196
+ if (tokensIn.length > 0 && tokensOut.length === 0) {
1197
+ primaryType = "bridge_in";
1198
+ const creditLeg = tokensIn[0];
1199
+ primaryAmount = creditLeg.amount;
1200
+ participant = creditLeg.accountId.replace("external:", "");
1201
+ } else if (tokensOut.length > 0 && tokensIn.length === 0) {
1202
+ primaryType = "bridge_out";
1203
+ const debitLeg = tokensOut[0];
1204
+ primaryAmount = debitLeg.amount;
1205
+ participant = debitLeg.accountId.replace("external:", "");
1206
+ } else if (tokensIn.length > 0 && tokensOut.length > 0) {
1207
+ primaryType = "bridge_in";
1208
+ const creditLeg = tokensIn[0];
1209
+ primaryAmount = creditLeg.amount;
1210
+ participant = creditLeg.accountId.replace("external:", "");
1211
+ } else {
1212
+ return null;
1213
+ }
1214
+ return {
1215
+ primaryType,
1216
+ primaryAmount,
1217
+ secondaryAmount: null,
1218
+ sender: primaryType === "bridge_out" ? participant : null,
1219
+ receiver: primaryType === "bridge_in" ? participant : null,
1220
+ counterparty: null,
1221
+ confidence: 0.9,
1222
+ isRelevant: true,
1223
+ metadata: {
1224
+ bridge_protocol: tx.protocol?.id,
1225
+ bridge_name: tx.protocol?.name
1226
+ }
1227
+ };
1228
+ }
1229
+ };
1230
+
1095
1231
  // ../classification/src/engine/classification-service.ts
1096
1232
  var ClassificationService = class {
1097
1233
  classifiers = [];
1098
1234
  constructor() {
1099
1235
  this.registerClassifier(new SolanaPayClassifier());
1236
+ this.registerClassifier(new BridgeClassifier());
1237
+ this.registerClassifier(new NftMintClassifier());
1238
+ this.registerClassifier(new StakeDepositClassifier());
1239
+ this.registerClassifier(new StakeWithdrawClassifier());
1100
1240
  this.registerClassifier(new SwapClassifier());
1101
1241
  this.registerClassifier(new AirdropClassifier());
1102
1242
  this.registerClassifier(new TransferClassifier());
@@ -1106,8 +1246,8 @@ var ClassificationService = class {
1106
1246
  this.classifiers.push(classifier);
1107
1247
  this.classifiers.sort((a, b) => b.priority - a.priority);
1108
1248
  }
1109
- classify(legs, walletAddress, tx) {
1110
- const context = { legs, walletAddress, tx };
1249
+ classify(legs, tx) {
1250
+ const context = { legs, tx };
1111
1251
  for (const classifier of this.classifiers) {
1112
1252
  const result = classifier.classify(context);
1113
1253
  if (result && result.isRelevant) {
@@ -1116,9 +1256,10 @@ var ClassificationService = class {
1116
1256
  }
1117
1257
  return {
1118
1258
  primaryType: "other",
1119
- direction: "neutral",
1120
1259
  primaryAmount: null,
1121
1260
  secondaryAmount: null,
1261
+ sender: null,
1262
+ receiver: null,
1122
1263
  counterparty: null,
1123
1264
  confidence: 0,
1124
1265
  isRelevant: false,
@@ -1127,8 +1268,8 @@ var ClassificationService = class {
1127
1268
  }
1128
1269
  };
1129
1270
  var classificationService = new ClassificationService();
1130
- function classifyTransaction(legs, walletAddress, tx) {
1131
- return classificationService.classify(legs, walletAddress, tx);
1271
+ function classifyTransaction(legs, tx) {
1272
+ return classificationService.classify(legs, tx);
1132
1273
  }
1133
1274
 
1134
1275
  // ../domain/src/tx/spam-filter.ts
@@ -1174,8 +1315,81 @@ function filterSpamTransactions(transactions, config) {
1174
1315
  );
1175
1316
  }
1176
1317
 
1318
+ // src/nft.ts
1319
+ async function fetchNftMetadata(rpcUrl, mintAddress) {
1320
+ const response = await fetch(rpcUrl, {
1321
+ method: "POST",
1322
+ headers: { "Content-Type": "application/json" },
1323
+ body: JSON.stringify({
1324
+ jsonrpc: "2.0",
1325
+ id: "get-asset",
1326
+ method: "getAsset",
1327
+ params: { id: mintAddress }
1328
+ })
1329
+ });
1330
+ const data = await response.json();
1331
+ if (data.error || !data.result?.content?.metadata) {
1332
+ return null;
1333
+ }
1334
+ const { result } = data;
1335
+ const { content, grouping } = result;
1336
+ return {
1337
+ mint: mintAddress,
1338
+ name: content.metadata.name,
1339
+ symbol: content.metadata.symbol,
1340
+ image: content.links.image ?? content.files?.[0]?.uri ?? "",
1341
+ cdnImage: content.files?.[0]?.cdn_uri,
1342
+ description: content.metadata.description,
1343
+ collection: grouping?.find((g) => g.group_key === "collection")?.group_value,
1344
+ attributes: content.metadata.attributes
1345
+ };
1346
+ }
1347
+ async function fetchNftMetadataBatch(rpcUrl, mintAddresses) {
1348
+ const results = await Promise.all(
1349
+ mintAddresses.map((mint) => fetchNftMetadata(rpcUrl, mint))
1350
+ );
1351
+ const map = /* @__PURE__ */ new Map();
1352
+ results.forEach((metadata, index) => {
1353
+ if (metadata) {
1354
+ map.set(mintAddresses[index], metadata);
1355
+ }
1356
+ });
1357
+ return map;
1358
+ }
1359
+
1177
1360
  // src/client.ts
1361
+ var NFT_TRANSACTION_TYPES = ["nft_mint", "nft_purchase", "nft_sale"];
1362
+ async function enrichNftClassification(rpcUrl, classified) {
1363
+ const { classification } = classified;
1364
+ if (!NFT_TRANSACTION_TYPES.includes(classification.primaryType)) {
1365
+ return classified;
1366
+ }
1367
+ const nftMint = classification.metadata?.nft_mint;
1368
+ if (!nftMint) {
1369
+ return classified;
1370
+ }
1371
+ const nftData = await fetchNftMetadata(rpcUrl, nftMint);
1372
+ if (!nftData) {
1373
+ return classified;
1374
+ }
1375
+ return {
1376
+ ...classified,
1377
+ classification: {
1378
+ ...classification,
1379
+ metadata: {
1380
+ ...classification.metadata,
1381
+ nft_name: nftData.name,
1382
+ nft_image: nftData.image,
1383
+ nft_cdn_image: nftData.cdnImage,
1384
+ nft_collection: nftData.collection,
1385
+ nft_symbol: nftData.symbol,
1386
+ nft_attributes: nftData.attributes
1387
+ }
1388
+ }
1389
+ };
1390
+ }
1178
1391
  function createIndexer(options) {
1392
+ const rpcUrl = "client" in options ? "" : options.rpcUrl;
1179
1393
  const client = "client" in options ? options.client : createSolanaClient(options.rpcUrl, options.wsUrl);
1180
1394
  return {
1181
1395
  rpc: client.rpc,
@@ -1183,7 +1397,39 @@ function createIndexer(options) {
1183
1397
  return fetchWalletBalance(client.rpc, walletAddress, tokenMints);
1184
1398
  },
1185
1399
  async getTransactions(walletAddress, options2 = {}) {
1186
- const { limit = 10, before, until, filterSpam = true, spamConfig } = options2;
1400
+ const { limit = 10, before, until, filterSpam = true, spamConfig, enrichNftMetadata = true } = options2;
1401
+ async function enrichBatch(transactions) {
1402
+ if (!enrichNftMetadata || !rpcUrl) {
1403
+ return transactions;
1404
+ }
1405
+ const nftMints = transactions.filter((t) => NFT_TRANSACTION_TYPES.includes(t.classification.primaryType)).map((t) => t.classification.metadata?.nft_mint).filter(Boolean);
1406
+ if (nftMints.length === 0) {
1407
+ return transactions;
1408
+ }
1409
+ const nftMetadataMap = await fetchNftMetadataBatch(rpcUrl, nftMints);
1410
+ return transactions.map((t) => {
1411
+ const nftMint = t.classification.metadata?.nft_mint;
1412
+ if (!nftMint || !nftMetadataMap.has(nftMint)) {
1413
+ return t;
1414
+ }
1415
+ const nftData = nftMetadataMap.get(nftMint);
1416
+ return {
1417
+ ...t,
1418
+ classification: {
1419
+ ...t.classification,
1420
+ metadata: {
1421
+ ...t.classification.metadata,
1422
+ nft_name: nftData.name,
1423
+ nft_image: nftData.image,
1424
+ nft_cdn_image: nftData.cdnImage,
1425
+ nft_collection: nftData.collection,
1426
+ nft_symbol: nftData.symbol,
1427
+ nft_attributes: nftData.attributes
1428
+ }
1429
+ }
1430
+ };
1431
+ });
1432
+ }
1187
1433
  if (!filterSpam) {
1188
1434
  const signatures = await fetchWalletSignatures(client.rpc, walletAddress, {
1189
1435
  limit,
@@ -1202,11 +1448,11 @@ function createIndexer(options) {
1202
1448
  );
1203
1449
  const classified = transactions.map((tx) => {
1204
1450
  tx.protocol = detectProtocol(tx.programIds);
1205
- const legs = transactionToLegs(tx, walletAddress);
1206
- const classification = classifyTransaction(legs, walletAddress, tx);
1451
+ const legs = transactionToLegs(tx);
1452
+ const classification = classifyTransaction(legs, tx);
1207
1453
  return { tx, classification, legs };
1208
1454
  });
1209
- return classified;
1455
+ return enrichBatch(classified);
1210
1456
  }
1211
1457
  const accumulated = [];
1212
1458
  let currentBefore = before;
@@ -1232,8 +1478,8 @@ function createIndexer(options) {
1232
1478
  );
1233
1479
  const classified = transactions.map((tx) => {
1234
1480
  tx.protocol = detectProtocol(tx.programIds);
1235
- const legs = transactionToLegs(tx, walletAddress);
1236
- const classification = classifyTransaction(legs, walletAddress, tx);
1481
+ const legs = transactionToLegs(tx);
1482
+ const classification = classifyTransaction(legs, tx);
1237
1483
  return { tx, classification, legs };
1238
1484
  });
1239
1485
  const nonSpam = filterSpamTransactions(classified, spamConfig);
@@ -1245,20 +1491,38 @@ function createIndexer(options) {
1245
1491
  break;
1246
1492
  }
1247
1493
  }
1248
- return accumulated.slice(0, limit);
1494
+ const result = accumulated.slice(0, limit);
1495
+ return enrichBatch(result);
1249
1496
  },
1250
- async getTransaction(signature2, walletAddress) {
1497
+ async getTransaction(signature2, options2 = {}) {
1498
+ const { enrichNftMetadata = true } = options2;
1251
1499
  const tx = await fetchTransaction(client.rpc, signature2);
1252
1500
  if (!tx) {
1253
1501
  return null;
1254
1502
  }
1255
1503
  tx.protocol = detectProtocol(tx.programIds);
1256
- const legs = transactionToLegs(tx, walletAddress);
1257
- const classification = classifyTransaction(legs, walletAddress, tx);
1258
- return { tx, classification, legs };
1504
+ const legs = transactionToLegs(tx);
1505
+ const classification = classifyTransaction(legs, tx);
1506
+ let classified = { tx, classification, legs };
1507
+ if (enrichNftMetadata && rpcUrl) {
1508
+ classified = await enrichNftClassification(rpcUrl, classified);
1509
+ }
1510
+ return classified;
1259
1511
  },
1260
1512
  async getRawTransaction(signature2) {
1261
1513
  return fetchTransaction(client.rpc, signature2);
1514
+ },
1515
+ async getNftMetadata(mintAddress) {
1516
+ if (!rpcUrl) {
1517
+ throw new Error("getNftMetadata requires rpcUrl to be set");
1518
+ }
1519
+ return fetchNftMetadata(rpcUrl, mintAddress);
1520
+ },
1521
+ async getNftMetadataBatch(mintAddresses) {
1522
+ if (!rpcUrl) {
1523
+ throw new Error("getNftMetadataBatch requires rpcUrl to be set");
1524
+ }
1525
+ return fetchNftMetadataBatch(rpcUrl, mintAddresses);
1262
1526
  }
1263
1527
  };
1264
1528
  }