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/index.js CHANGED
@@ -149,6 +149,7 @@ function extractProgramIds(transaction) {
149
149
  var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
150
150
  var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
151
151
  var STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111";
152
+ var STAKE_POOL_PROGRAM_ID = "SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy";
152
153
  var TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
153
154
  var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
154
155
  var SPL_MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
@@ -158,6 +159,15 @@ var JUPITER_V4_PROGRAM_ID = "JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB";
158
159
  var RAYDIUM_PROGRAM_ID = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
159
160
  var ORCA_WHIRLPOOL_PROGRAM_ID = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc";
160
161
  var METAPLEX_PROGRAM_ID = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
162
+ var CANDY_MACHINE_V3_PROGRAM_ID = "CndyV3LdqHUfDLmE5naZjVN8rBZz4tqhdefbAnjHG3JR";
163
+ var CANDY_GUARD_PROGRAM_ID = "Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g";
164
+ var BUBBLEGUM_PROGRAM_ID = "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY";
165
+ var MAGIC_EDEN_CANDY_MACHINE_ID = "CMZYPASGWeTz7RNGHaRJfCq2XQ5pYK6nDvVQxzkH51zb";
166
+ var WORMHOLE_PROGRAM_ID = "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth";
167
+ var WORMHOLE_TOKEN_BRIDGE_ID = "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb";
168
+ var DEGODS_BRIDGE_PROGRAM_ID = "35iLrpYNNR9ygHLcvE1xKFHbHq6paHthrF6wSovdWgGu";
169
+ var DEBRIDGE_PROGRAM_ID = "DEbrdGj3HsRsAzx6uH4MKyREKxVAfBydijLUF3ygsFfh";
170
+ var ALLBRIDGE_PROGRAM_ID = "BrdgN2RPzEMWF96ZbnnJaUtQDQx7VRXYaHHbYCBvceWB";
161
171
  var PAYAI_FACILITATOR = "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4";
162
172
  var KNOWN_FACILITATORS = [
163
173
  PAYAI_FACILITATOR
@@ -418,6 +428,7 @@ async function fetchTransaction(rpc, signature2, commitment = "confirmed") {
418
428
  signature: signature2,
419
429
  slot: response.slot,
420
430
  blockTime: response.blockTime,
431
+ fee: Number(response.meta?.fee ?? 0),
421
432
  err: response.meta?.err ?? null,
422
433
  programIds: extractProgramIds(response.transaction),
423
434
  protocol: null,
@@ -600,7 +611,7 @@ var DEX_PROTOCOL_IDS = /* @__PURE__ */ new Set([
600
611
  function isDexProtocol(protocol) {
601
612
  return protocol !== null && protocol !== void 0 && DEX_PROTOCOL_IDS.has(protocol.id);
602
613
  }
603
- function transactionToLegs(tx, walletAddress) {
614
+ function transactionToLegs(tx) {
604
615
  const legs = [];
605
616
  const feePayer = tx.accountKeys?.[0]?.toLowerCase();
606
617
  const solChanges = extractSolBalanceChanges(tx);
@@ -608,9 +619,8 @@ function transactionToLegs(tx, walletAddress) {
608
619
  let totalSolCredits = 0n;
609
620
  for (const change of solChanges) {
610
621
  if (change.change === 0n) continue;
611
- const isWallet = walletAddress ? change.address.toLowerCase() === walletAddress.toLowerCase() : false;
612
622
  const accountId = buildAccountId({
613
- type: isWallet ? "wallet" : "external",
623
+ type: "external",
614
624
  address: change.address
615
625
  });
616
626
  const solInfo = TOKEN_INFO[KNOWN_TOKENS.SOL];
@@ -630,7 +640,7 @@ function transactionToLegs(tx, walletAddress) {
630
640
  amountRaw: change.change.toString().replace("-", ""),
631
641
  amountUi: Math.abs(change.changeUi)
632
642
  },
633
- role: determineSolRole(change, walletAddress, tx, feePayer)
643
+ role: determineSolRole(change, tx, feePayer)
634
644
  });
635
645
  }
636
646
  const networkFee = totalSolDebits - totalSolCredits;
@@ -652,26 +662,22 @@ function transactionToLegs(tx, walletAddress) {
652
662
  const tokenChanges = extractTokenBalanceChanges(tx);
653
663
  for (const change of tokenChanges) {
654
664
  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
665
  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
- });
666
+ if (isDexProtocol(tx.protocol)) {
667
+ const isFeePayer = feePayer && change.owner?.toLowerCase() === feePayer;
668
+ if (isFeePayer) {
669
+ accountId = buildAccountId({
670
+ type: "external",
671
+ address: change.owner || feePayer
672
+ });
673
+ } else {
674
+ accountId = buildAccountId({
675
+ type: "protocol",
676
+ address: change.owner || change.tokenInfo.mint,
677
+ protocol: tx.protocol.id,
678
+ token: change.tokenInfo.symbol
679
+ });
680
+ }
675
681
  } else {
676
682
  accountId = buildAccountId({
677
683
  type: "external",
@@ -686,41 +692,30 @@ function transactionToLegs(tx, walletAddress) {
686
692
  amountRaw: change.change.raw.replace("-", ""),
687
693
  amountUi: Math.abs(change.change.ui)
688
694
  },
689
- role: determineTokenRole(change, walletAddress, tx, feePayer)
695
+ role: determineTokenRole(change, tx, feePayer)
690
696
  });
691
697
  }
692
698
  return legs;
693
699
  }
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;
700
+ function determineSolRole(change, tx, feePayer) {
701
+ const isFeePayer = feePayer ? change.address.toLowerCase() === feePayer : false;
697
702
  const isPositive = change.change > 0n;
698
703
  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
704
  if (isFeePayer && !isPositive && amountSol < 0.01) {
712
705
  return "fee";
713
706
  }
714
707
  if (isPositive) {
708
+ if (tx.protocol?.id === "stake") {
709
+ return "reward";
710
+ }
715
711
  return "received";
716
712
  }
717
713
  return "sent";
718
714
  }
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;
715
+ function determineTokenRole(change, tx, feePayer) {
716
+ const isFeePayer = feePayer ? change.owner?.toLowerCase() === feePayer : false;
722
717
  const isPositive = change.change.ui > 0;
723
- if (isWallet || isFeePayer) {
718
+ if (isFeePayer) {
724
719
  return isPositive ? "received" : "sent";
725
720
  }
726
721
  if (isDexProtocol(tx.protocol)) {
@@ -734,85 +729,35 @@ var TransferClassifier = class {
734
729
  name = "transfer";
735
730
  priority = 20;
736
731
  classify(context) {
737
- const { legs, walletAddress, tx } = context;
732
+ const { legs, tx } = context;
738
733
  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"
734
+ const senderLeg = legs.find(
735
+ (l) => l.accountId.startsWith("external:") && l.side === "debit" && l.role === "sent"
761
736
  );
762
- const otherSent = legs.filter(
763
- (l) => !l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.accountId.startsWith("external:") && l.side === "debit" && l.role === "sent"
737
+ if (!senderLeg) return null;
738
+ const sender = senderLeg.accountId.replace("external:", "");
739
+ const receiverLeg = legs.find(
740
+ (l) => l.accountId.startsWith("external:") && l.side === "credit" && l.role === "received" && l.amount.token.mint === senderLeg.amount.token.mint
764
741
  );
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
- };
742
+ if (!receiverLeg) return null;
743
+ const receiver = receiverLeg.accountId.replace("external:", "");
744
+ return {
745
+ primaryType: "transfer",
746
+ primaryAmount: senderLeg.amount,
747
+ secondaryAmount: null,
748
+ sender,
749
+ receiver,
750
+ counterparty: {
751
+ type: "wallet",
752
+ address: receiver,
753
+ name: `${receiver.slice(0, 8)}...`
754
+ },
755
+ confidence: 0.95,
756
+ isRelevant: true,
757
+ metadata: {
758
+ ...facilitator && { facilitator, payment_type: "facilitated" }
813
759
  }
814
- }
815
- return null;
760
+ };
816
761
  }
817
762
  };
818
763
 
@@ -857,9 +802,54 @@ var KNOWN_PROGRAMS = {
857
802
  [STAKE_PROGRAM_ID]: {
858
803
  id: "stake",
859
804
  name: "Stake Program"
805
+ },
806
+ [STAKE_POOL_PROGRAM_ID]: {
807
+ id: "stake-pool",
808
+ name: "Stake Pool Program"
809
+ },
810
+ [CANDY_GUARD_PROGRAM_ID]: {
811
+ id: "candy-guard",
812
+ name: "Metaplex Candy Guard Program"
813
+ },
814
+ [CANDY_MACHINE_V3_PROGRAM_ID]: {
815
+ id: "candy-machine-v3",
816
+ name: "Metaplex Candy Machine Core Program"
817
+ },
818
+ [BUBBLEGUM_PROGRAM_ID]: {
819
+ id: "bubblegum",
820
+ name: "Bubblegum Program"
821
+ },
822
+ [MAGIC_EDEN_CANDY_MACHINE_ID]: {
823
+ id: "magic-eden-candy-machine",
824
+ name: "Nft Candy Machine Program (Magic Eden)"
825
+ },
826
+ [WORMHOLE_PROGRAM_ID]: {
827
+ id: "wormhole",
828
+ name: "Wormhole"
829
+ },
830
+ [WORMHOLE_TOKEN_BRIDGE_ID]: {
831
+ id: "wormhole-token-bridge",
832
+ name: "Wormhole Token Bridge"
833
+ },
834
+ [DEGODS_BRIDGE_PROGRAM_ID]: {
835
+ id: "degods-bridge",
836
+ name: "DeGods Bridge"
837
+ },
838
+ [DEBRIDGE_PROGRAM_ID]: {
839
+ id: "debridge",
840
+ name: "deBridge"
841
+ },
842
+ [ALLBRIDGE_PROGRAM_ID]: {
843
+ id: "allbridge",
844
+ name: "Allbridge"
860
845
  }
861
846
  };
862
847
  var PRIORITY_ORDER = [
848
+ "wormhole",
849
+ "wormhole-token-bridge",
850
+ "degods-bridge",
851
+ "debridge",
852
+ "allbridge",
863
853
  "jupiter",
864
854
  "jupiter-v4",
865
855
  "raydium",
@@ -877,9 +867,33 @@ var DEX_PROTOCOL_IDS2 = /* @__PURE__ */ new Set([
877
867
  "raydium",
878
868
  "orca-whirlpool"
879
869
  ]);
870
+ var NFT_MINT_PROTOCOL_IDS = /* @__PURE__ */ new Set([
871
+ "metaplex",
872
+ "candy-machine-v3",
873
+ "candy-guard",
874
+ "bubblegum",
875
+ "magic-eden-candy-machine"
876
+ ]);
877
+ var STAKE_PROTOCOL_IDS = /* @__PURE__ */ new Set(["stake", "stake-pool"]);
878
+ var BRIDGE_PROTOCOL_IDS = /* @__PURE__ */ new Set([
879
+ "wormhole",
880
+ "wormhole-token-bridge",
881
+ "degods-bridge",
882
+ "debridge",
883
+ "allbridge"
884
+ ]);
880
885
  function isDexProtocolById(protocolId) {
881
886
  return protocolId !== void 0 && DEX_PROTOCOL_IDS2.has(protocolId);
882
887
  }
888
+ function isNftMintProtocolById(protocolId) {
889
+ return protocolId !== void 0 && NFT_MINT_PROTOCOL_IDS.has(protocolId);
890
+ }
891
+ function isStakeProtocolById(protocolId) {
892
+ return protocolId !== void 0 && STAKE_PROTOCOL_IDS.has(protocolId);
893
+ }
894
+ function isBridgeProtocolById(protocolId) {
895
+ return protocolId !== void 0 && BRIDGE_PROTOCOL_IDS.has(protocolId);
896
+ }
883
897
  function detectProtocol(programIds) {
884
898
  const detectedProtocols = [];
885
899
  for (const programId of programIds) {
@@ -904,65 +918,46 @@ var SwapClassifier = class {
904
918
  name = "swap";
905
919
  priority = 80;
906
920
  classify(context) {
907
- const { legs, walletAddress, tx } = context;
908
- const protocolLegs = legs.filter(
909
- (leg) => leg.accountId.startsWith("protocol:")
910
- );
921
+ const { legs, tx } = context;
911
922
  const hasDexProtocol = isDexProtocolById(tx.protocol?.id);
912
- if (protocolLegs.length === 0 && !hasDexProtocol) {
923
+ if (!hasDexProtocol) {
913
924
  return null;
914
925
  }
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"
926
+ const feeLeg = legs.find(
927
+ (leg) => leg.role === "fee" && leg.side === "debit"
932
928
  );
933
- const tokensOut = participantDebits.filter(
934
- (leg) => leg.role === "sent" || leg.role === "protocol_deposit"
929
+ const initiator = feeLeg?.accountId.replace("external:", "") ?? null;
930
+ const tokensOut = legs.filter(
931
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && (leg.role === "sent" || leg.role === "protocol_deposit")
935
932
  );
936
- const tokensIn = participantCredits.filter(
937
- (leg) => leg.role === "received" || leg.role === "protocol_withdraw"
933
+ const tokensIn = legs.filter(
934
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && (leg.role === "received" || leg.role === "protocol_withdraw")
938
935
  );
939
936
  if (tokensOut.length === 0 || tokensIn.length === 0) {
940
937
  return null;
941
938
  }
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
- }
939
+ const tokenOut = tokensOut[0];
940
+ const tokenIn = tokensIn[0];
941
+ if (tokenOut.amount.token.symbol === tokenIn.amount.token.symbol) {
942
+ return null;
964
943
  }
965
- return null;
944
+ return {
945
+ primaryType: "swap",
946
+ primaryAmount: tokenOut.amount,
947
+ secondaryAmount: tokenIn.amount,
948
+ sender: initiator,
949
+ receiver: initiator,
950
+ counterparty: null,
951
+ confidence: 0.9,
952
+ isRelevant: true,
953
+ metadata: {
954
+ swap_type: "token_to_token",
955
+ from_token: tokenOut.amount.token.symbol,
956
+ to_token: tokenIn.amount.token.symbol,
957
+ from_amount: tokenOut.amount.amountUi,
958
+ to_amount: tokenIn.amount.amountUi
959
+ }
960
+ };
966
961
  }
967
962
  };
968
963
 
@@ -971,7 +966,7 @@ var AirdropClassifier = class {
971
966
  name = "airdrop";
972
967
  priority = 70;
973
968
  classify(context) {
974
- const { legs, walletAddress, tx } = context;
969
+ const { legs, tx } = context;
975
970
  const facilitator = tx.accountKeys ? detectFacilitator(tx.accountKeys) : null;
976
971
  const protocolLegs = legs.filter(
977
972
  (leg) => leg.accountId.startsWith("protocol:")
@@ -979,49 +974,41 @@ var AirdropClassifier = class {
979
974
  if (protocolLegs.length === 0) {
980
975
  return null;
981
976
  }
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"
977
+ const tokenReceived = legs.filter(
978
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.role === "received" && leg.amount.token.symbol !== "SOL"
992
979
  );
993
980
  if (tokenReceived.length === 0) {
994
981
  return null;
995
982
  }
996
- const tokenSent = participantDebits.filter(
997
- (leg) => leg.role === "sent" && leg.amount.token.symbol !== "SOL"
983
+ const tokenSent = legs.filter(
984
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role === "sent" && leg.amount.token.symbol !== "SOL"
998
985
  );
999
986
  if (tokenSent.length > 0) {
1000
987
  return null;
1001
988
  }
1002
989
  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)
990
+ const receiver = mainToken.accountId.replace("external:", "");
991
+ const senderLeg = legs.find(
992
+ (leg) => leg.side === "debit" && leg.amount.token.mint === mainToken.amount.token.mint
1005
993
  );
1006
- const sender = senderLegs.length > 0 ? senderLegs[0] : null;
1007
- const receiverAddress = mainToken.accountId.replace(/^(external:|wallet:)/, "");
994
+ const sender = senderLeg ? senderLeg.accountId.replace(/^(external:|protocol:)/, "") : null;
1008
995
  return {
1009
996
  primaryType: "airdrop",
1010
- direction: isObserverMode ? "neutral" : "incoming",
1011
997
  primaryAmount: mainToken.amount,
1012
998
  secondaryAmount: null,
999
+ sender,
1000
+ receiver,
1013
1001
  counterparty: sender ? {
1014
- type: "unknown",
1015
- address: sender.accountId.replace(/^(external:|protocol:|wallet:)/, "")
1002
+ type: "protocol",
1003
+ address: sender
1016
1004
  } : null,
1017
- confidence: isObserverMode ? 0.8 : 0.85,
1005
+ confidence: 0.85,
1018
1006
  isRelevant: true,
1019
1007
  metadata: {
1020
1008
  airdrop_type: "token",
1021
1009
  token: mainToken.amount.token.symbol,
1022
1010
  amount: mainToken.amount.amountUi,
1023
- ...isObserverMode && { observer_mode: true, receiver: receiverAddress },
1024
- ...facilitator && { facilitator, payment_type: "facilitated" }
1011
+ ...facilitator && { facilitator }
1025
1012
  }
1026
1013
  };
1027
1014
  }
@@ -1032,30 +1019,34 @@ var FeeOnlyClassifier = class {
1032
1019
  name = "fee-only";
1033
1020
  priority = 60;
1034
1021
  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) {
1022
+ const { legs } = context;
1023
+ const externalLegs = legs.filter(
1024
+ (leg) => leg.accountId.startsWith("external:")
1025
+ );
1026
+ const nonFeeLegs = externalLegs.filter((leg) => leg.role !== "fee");
1027
+ if (nonFeeLegs.length > 0) {
1041
1028
  return null;
1042
1029
  }
1043
1030
  const feeLegs = legs.filter((leg) => leg.role === "fee");
1044
1031
  if (feeLegs.length === 0) {
1045
1032
  return null;
1046
1033
  }
1034
+ const feePayerLeg = feeLegs.find(
1035
+ (leg) => leg.side === "debit" && leg.amount.token.symbol === "SOL"
1036
+ );
1037
+ const feePayer = feePayerLeg?.accountId.replace("external:", "") ?? null;
1047
1038
  const totalFee = feeLegs.find((leg) => leg.amount.token.symbol === "SOL");
1048
1039
  return {
1049
1040
  primaryType: "fee_only",
1050
- direction: "outgoing",
1051
1041
  primaryAmount: totalFee?.amount ?? null,
1052
1042
  secondaryAmount: null,
1043
+ sender: feePayer,
1044
+ receiver: null,
1053
1045
  counterparty: null,
1054
- confidence: isObserverMode ? 0.9 : 0.95,
1046
+ confidence: 0.95,
1055
1047
  isRelevant: false,
1056
1048
  metadata: {
1057
- fee_type: "network",
1058
- ...isObserverMode && { observer_mode: true }
1049
+ fee_type: "network"
1059
1050
  }
1060
1051
  };
1061
1052
  }
@@ -1066,59 +1057,31 @@ var SolanaPayClassifier = class {
1066
1057
  name = "solana-pay";
1067
1058
  priority = 95;
1068
1059
  classify(context) {
1069
- const { tx, walletAddress, legs } = context;
1060
+ const { tx, legs } = context;
1070
1061
  if (!isSolanaPayTransaction(tx.programIds, tx.memo)) {
1071
1062
  return null;
1072
1063
  }
1073
1064
  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"
1065
+ const senderLeg = legs.find(
1066
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role === "sent"
1110
1067
  );
1111
- const userReceived = legs.find(
1112
- (leg) => leg.accountId.includes(walletPrefix) && leg.side === "credit" && leg.role === "received"
1068
+ const receiverLeg = legs.find(
1069
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.role === "received"
1113
1070
  );
1114
- const direction = userSent ? "outgoing" : "incoming";
1115
- const primaryAmount = userSent?.amount ?? userReceived?.amount ?? null;
1071
+ const sender = senderLeg?.accountId.replace("external:", "") ?? null;
1072
+ const receiver = receiverLeg?.accountId.replace("external:", "") ?? null;
1073
+ const primaryAmount = senderLeg?.amount ?? receiverLeg?.amount ?? null;
1116
1074
  return {
1117
1075
  primaryType: "transfer",
1118
- direction,
1119
1076
  primaryAmount,
1120
1077
  secondaryAmount: null,
1121
- counterparty: memo.merchant ? { address: "", name: memo.merchant, type: "merchant" } : null,
1078
+ sender,
1079
+ receiver,
1080
+ counterparty: receiver ? {
1081
+ address: receiver,
1082
+ name: memo.merchant ?? void 0,
1083
+ type: memo.merchant ? "merchant" : "wallet"
1084
+ } : null,
1122
1085
  confidence: 0.98,
1123
1086
  isRelevant: true,
1124
1087
  metadata: {
@@ -1134,11 +1097,188 @@ var SolanaPayClassifier = class {
1134
1097
  }
1135
1098
  };
1136
1099
 
1100
+ // ../classification/src/classifiers/nft-mint-classifier.ts
1101
+ var NftMintClassifier = class {
1102
+ name = "nft-mint";
1103
+ priority = 85;
1104
+ classify(context) {
1105
+ const { legs, tx } = context;
1106
+ const hasNftMintProtocol = isNftMintProtocolById(tx.protocol?.id);
1107
+ if (!hasNftMintProtocol) {
1108
+ return null;
1109
+ }
1110
+ const nftCredits = legs.filter(
1111
+ (leg) => leg.side === "credit" && leg.amount.token.decimals === 0 && leg.amount.amountUi >= 1 && (leg.role === "received" || leg.role === "protocol_withdraw")
1112
+ );
1113
+ if (nftCredits.length === 0) {
1114
+ return null;
1115
+ }
1116
+ const primaryNft = nftCredits[0];
1117
+ const minter = primaryNft.accountId.replace("external:", "");
1118
+ const paymentLeg = legs.find(
1119
+ (leg) => leg.side === "debit" && leg.role === "sent" && leg.amount.token.symbol === "SOL"
1120
+ );
1121
+ const totalMinted = nftCredits.reduce(
1122
+ (sum, leg) => sum + leg.amount.amountUi,
1123
+ 0
1124
+ );
1125
+ return {
1126
+ primaryType: "nft_mint",
1127
+ primaryAmount: primaryNft.amount,
1128
+ secondaryAmount: paymentLeg?.amount ?? null,
1129
+ sender: null,
1130
+ receiver: minter,
1131
+ counterparty: null,
1132
+ confidence: 0.9,
1133
+ isRelevant: true,
1134
+ metadata: {
1135
+ nft_mint: primaryNft.amount.token.mint,
1136
+ nft_name: primaryNft.amount.token.name,
1137
+ quantity: totalMinted,
1138
+ mint_price: paymentLeg?.amount.amountUi,
1139
+ protocol: tx.protocol?.id
1140
+ }
1141
+ };
1142
+ }
1143
+ };
1144
+
1145
+ // ../classification/src/classifiers/stake-deposit-classifier.ts
1146
+ var StakeDepositClassifier = class {
1147
+ name = "stake-deposit";
1148
+ priority = 82;
1149
+ classify(context) {
1150
+ const { legs, tx } = context;
1151
+ const hasStakeProtocol = isStakeProtocolById(tx.protocol?.id);
1152
+ if (!hasStakeProtocol) {
1153
+ return null;
1154
+ }
1155
+ const solDebit = legs.find(
1156
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.amount.token.symbol === "SOL" && (leg.role === "sent" || leg.role === "protocol_deposit")
1157
+ );
1158
+ if (!solDebit) {
1159
+ return null;
1160
+ }
1161
+ const staker = solDebit.accountId.replace("external:", "");
1162
+ return {
1163
+ primaryType: "stake_deposit",
1164
+ primaryAmount: solDebit.amount,
1165
+ secondaryAmount: null,
1166
+ sender: staker,
1167
+ receiver: null,
1168
+ counterparty: null,
1169
+ confidence: 0.9,
1170
+ isRelevant: true,
1171
+ metadata: {
1172
+ stake_amount: solDebit.amount.amountUi,
1173
+ protocol: tx.protocol?.id
1174
+ }
1175
+ };
1176
+ }
1177
+ };
1178
+
1179
+ // ../classification/src/classifiers/stake-withdraw-classifier.ts
1180
+ var StakeWithdrawClassifier = class {
1181
+ name = "stake-withdraw";
1182
+ priority = 81;
1183
+ classify(context) {
1184
+ const { legs, tx } = context;
1185
+ const hasStakeProtocol = isStakeProtocolById(tx.protocol?.id);
1186
+ if (!hasStakeProtocol) {
1187
+ return null;
1188
+ }
1189
+ const solCredit = legs.find(
1190
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.amount.token.symbol === "SOL" && (leg.role === "received" || leg.role === "protocol_withdraw")
1191
+ );
1192
+ if (!solCredit) {
1193
+ return null;
1194
+ }
1195
+ const solDebit = legs.find(
1196
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.amount.token.symbol === "SOL" && (leg.role === "sent" || leg.role === "protocol_deposit")
1197
+ );
1198
+ if (solDebit) {
1199
+ return null;
1200
+ }
1201
+ const withdrawer = solCredit.accountId.replace("external:", "");
1202
+ return {
1203
+ primaryType: "stake_withdraw",
1204
+ primaryAmount: solCredit.amount,
1205
+ secondaryAmount: null,
1206
+ sender: null,
1207
+ receiver: withdrawer,
1208
+ counterparty: null,
1209
+ confidence: 0.9,
1210
+ isRelevant: true,
1211
+ metadata: {
1212
+ withdraw_amount: solCredit.amount.amountUi,
1213
+ protocol: tx.protocol?.id
1214
+ }
1215
+ };
1216
+ }
1217
+ };
1218
+
1219
+ // ../classification/src/classifiers/bridge-classifier.ts
1220
+ var BridgeClassifier = class {
1221
+ name = "bridge";
1222
+ priority = 88;
1223
+ classify(context) {
1224
+ const { legs, tx } = context;
1225
+ const hasBridgeProtocol = isBridgeProtocolById(tx.protocol?.id);
1226
+ if (!hasBridgeProtocol) {
1227
+ return null;
1228
+ }
1229
+ const tokensOut = legs.filter(
1230
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role !== "fee" && (leg.role === "sent" || leg.role === "protocol_deposit")
1231
+ );
1232
+ const tokensIn = legs.filter(
1233
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && (leg.role === "received" || leg.role === "protocol_withdraw")
1234
+ );
1235
+ let primaryType;
1236
+ let primaryAmount = null;
1237
+ let participant = null;
1238
+ if (tokensIn.length > 0 && tokensOut.length === 0) {
1239
+ primaryType = "bridge_in";
1240
+ const creditLeg = tokensIn[0];
1241
+ primaryAmount = creditLeg.amount;
1242
+ participant = creditLeg.accountId.replace("external:", "");
1243
+ } else if (tokensOut.length > 0 && tokensIn.length === 0) {
1244
+ primaryType = "bridge_out";
1245
+ const debitLeg = tokensOut[0];
1246
+ primaryAmount = debitLeg.amount;
1247
+ participant = debitLeg.accountId.replace("external:", "");
1248
+ } else if (tokensIn.length > 0 && tokensOut.length > 0) {
1249
+ primaryType = "bridge_in";
1250
+ const creditLeg = tokensIn[0];
1251
+ primaryAmount = creditLeg.amount;
1252
+ participant = creditLeg.accountId.replace("external:", "");
1253
+ } else {
1254
+ return null;
1255
+ }
1256
+ return {
1257
+ primaryType,
1258
+ primaryAmount,
1259
+ secondaryAmount: null,
1260
+ sender: primaryType === "bridge_out" ? participant : null,
1261
+ receiver: primaryType === "bridge_in" ? participant : null,
1262
+ counterparty: null,
1263
+ confidence: 0.9,
1264
+ isRelevant: true,
1265
+ metadata: {
1266
+ bridge_protocol: tx.protocol?.id,
1267
+ bridge_name: tx.protocol?.name
1268
+ }
1269
+ };
1270
+ }
1271
+ };
1272
+
1137
1273
  // ../classification/src/engine/classification-service.ts
1138
1274
  var ClassificationService = class {
1139
1275
  classifiers = [];
1140
1276
  constructor() {
1141
1277
  this.registerClassifier(new SolanaPayClassifier());
1278
+ this.registerClassifier(new BridgeClassifier());
1279
+ this.registerClassifier(new NftMintClassifier());
1280
+ this.registerClassifier(new StakeDepositClassifier());
1281
+ this.registerClassifier(new StakeWithdrawClassifier());
1142
1282
  this.registerClassifier(new SwapClassifier());
1143
1283
  this.registerClassifier(new AirdropClassifier());
1144
1284
  this.registerClassifier(new TransferClassifier());
@@ -1148,8 +1288,8 @@ var ClassificationService = class {
1148
1288
  this.classifiers.push(classifier);
1149
1289
  this.classifiers.sort((a, b) => b.priority - a.priority);
1150
1290
  }
1151
- classify(legs, walletAddress, tx) {
1152
- const context = { legs, walletAddress, tx };
1291
+ classify(legs, tx) {
1292
+ const context = { legs, tx };
1153
1293
  for (const classifier of this.classifiers) {
1154
1294
  const result = classifier.classify(context);
1155
1295
  if (result && result.isRelevant) {
@@ -1158,9 +1298,10 @@ var ClassificationService = class {
1158
1298
  }
1159
1299
  return {
1160
1300
  primaryType: "other",
1161
- direction: "neutral",
1162
1301
  primaryAmount: null,
1163
1302
  secondaryAmount: null,
1303
+ sender: null,
1304
+ receiver: null,
1164
1305
  counterparty: null,
1165
1306
  confidence: 0,
1166
1307
  isRelevant: false,
@@ -1169,8 +1310,8 @@ var ClassificationService = class {
1169
1310
  }
1170
1311
  };
1171
1312
  var classificationService = new ClassificationService();
1172
- function classifyTransaction(legs, walletAddress, tx) {
1173
- return classificationService.classify(legs, walletAddress, tx);
1313
+ function classifyTransaction(legs, tx) {
1314
+ return classificationService.classify(legs, tx);
1174
1315
  }
1175
1316
 
1176
1317
  // ../domain/src/tx/spam-filter.ts
@@ -1216,8 +1357,81 @@ function filterSpamTransactions(transactions, config) {
1216
1357
  );
1217
1358
  }
1218
1359
 
1360
+ // src/nft.ts
1361
+ async function fetchNftMetadata(rpcUrl, mintAddress) {
1362
+ const response = await fetch(rpcUrl, {
1363
+ method: "POST",
1364
+ headers: { "Content-Type": "application/json" },
1365
+ body: JSON.stringify({
1366
+ jsonrpc: "2.0",
1367
+ id: "get-asset",
1368
+ method: "getAsset",
1369
+ params: { id: mintAddress }
1370
+ })
1371
+ });
1372
+ const data = await response.json();
1373
+ if (data.error || !data.result?.content?.metadata) {
1374
+ return null;
1375
+ }
1376
+ const { result } = data;
1377
+ const { content, grouping } = result;
1378
+ return {
1379
+ mint: mintAddress,
1380
+ name: content.metadata.name,
1381
+ symbol: content.metadata.symbol,
1382
+ image: content.links.image ?? content.files?.[0]?.uri ?? "",
1383
+ cdnImage: content.files?.[0]?.cdn_uri,
1384
+ description: content.metadata.description,
1385
+ collection: grouping?.find((g) => g.group_key === "collection")?.group_value,
1386
+ attributes: content.metadata.attributes
1387
+ };
1388
+ }
1389
+ async function fetchNftMetadataBatch(rpcUrl, mintAddresses) {
1390
+ const results = await Promise.all(
1391
+ mintAddresses.map((mint) => fetchNftMetadata(rpcUrl, mint))
1392
+ );
1393
+ const map = /* @__PURE__ */ new Map();
1394
+ results.forEach((metadata, index) => {
1395
+ if (metadata) {
1396
+ map.set(mintAddresses[index], metadata);
1397
+ }
1398
+ });
1399
+ return map;
1400
+ }
1401
+
1219
1402
  // src/client.ts
1403
+ var NFT_TRANSACTION_TYPES = ["nft_mint", "nft_purchase", "nft_sale"];
1404
+ async function enrichNftClassification(rpcUrl, classified) {
1405
+ const { classification } = classified;
1406
+ if (!NFT_TRANSACTION_TYPES.includes(classification.primaryType)) {
1407
+ return classified;
1408
+ }
1409
+ const nftMint = classification.metadata?.nft_mint;
1410
+ if (!nftMint) {
1411
+ return classified;
1412
+ }
1413
+ const nftData = await fetchNftMetadata(rpcUrl, nftMint);
1414
+ if (!nftData) {
1415
+ return classified;
1416
+ }
1417
+ return {
1418
+ ...classified,
1419
+ classification: {
1420
+ ...classification,
1421
+ metadata: {
1422
+ ...classification.metadata,
1423
+ nft_name: nftData.name,
1424
+ nft_image: nftData.image,
1425
+ nft_cdn_image: nftData.cdnImage,
1426
+ nft_collection: nftData.collection,
1427
+ nft_symbol: nftData.symbol,
1428
+ nft_attributes: nftData.attributes
1429
+ }
1430
+ }
1431
+ };
1432
+ }
1220
1433
  function createIndexer(options) {
1434
+ const rpcUrl = "client" in options ? "" : options.rpcUrl;
1221
1435
  const client = "client" in options ? options.client : createSolanaClient(options.rpcUrl, options.wsUrl);
1222
1436
  return {
1223
1437
  rpc: client.rpc,
@@ -1225,7 +1439,39 @@ function createIndexer(options) {
1225
1439
  return fetchWalletBalance(client.rpc, walletAddress, tokenMints);
1226
1440
  },
1227
1441
  async getTransactions(walletAddress, options2 = {}) {
1228
- const { limit = 10, before, until, filterSpam = true, spamConfig } = options2;
1442
+ const { limit = 10, before, until, filterSpam = true, spamConfig, enrichNftMetadata = true } = options2;
1443
+ async function enrichBatch(transactions) {
1444
+ if (!enrichNftMetadata || !rpcUrl) {
1445
+ return transactions;
1446
+ }
1447
+ const nftMints = transactions.filter((t) => NFT_TRANSACTION_TYPES.includes(t.classification.primaryType)).map((t) => t.classification.metadata?.nft_mint).filter(Boolean);
1448
+ if (nftMints.length === 0) {
1449
+ return transactions;
1450
+ }
1451
+ const nftMetadataMap = await fetchNftMetadataBatch(rpcUrl, nftMints);
1452
+ return transactions.map((t) => {
1453
+ const nftMint = t.classification.metadata?.nft_mint;
1454
+ if (!nftMint || !nftMetadataMap.has(nftMint)) {
1455
+ return t;
1456
+ }
1457
+ const nftData = nftMetadataMap.get(nftMint);
1458
+ return {
1459
+ ...t,
1460
+ classification: {
1461
+ ...t.classification,
1462
+ metadata: {
1463
+ ...t.classification.metadata,
1464
+ nft_name: nftData.name,
1465
+ nft_image: nftData.image,
1466
+ nft_cdn_image: nftData.cdnImage,
1467
+ nft_collection: nftData.collection,
1468
+ nft_symbol: nftData.symbol,
1469
+ nft_attributes: nftData.attributes
1470
+ }
1471
+ }
1472
+ };
1473
+ });
1474
+ }
1229
1475
  if (!filterSpam) {
1230
1476
  const signatures = await fetchWalletSignatures(client.rpc, walletAddress, {
1231
1477
  limit,
@@ -1244,11 +1490,11 @@ function createIndexer(options) {
1244
1490
  );
1245
1491
  const classified = transactions.map((tx) => {
1246
1492
  tx.protocol = detectProtocol(tx.programIds);
1247
- const legs = transactionToLegs(tx, walletAddress);
1248
- const classification = classifyTransaction(legs, walletAddress, tx);
1493
+ const legs = transactionToLegs(tx);
1494
+ const classification = classifyTransaction(legs, tx);
1249
1495
  return { tx, classification, legs };
1250
1496
  });
1251
- return classified;
1497
+ return enrichBatch(classified);
1252
1498
  }
1253
1499
  const accumulated = [];
1254
1500
  let currentBefore = before;
@@ -1274,8 +1520,8 @@ function createIndexer(options) {
1274
1520
  );
1275
1521
  const classified = transactions.map((tx) => {
1276
1522
  tx.protocol = detectProtocol(tx.programIds);
1277
- const legs = transactionToLegs(tx, walletAddress);
1278
- const classification = classifyTransaction(legs, walletAddress, tx);
1523
+ const legs = transactionToLegs(tx);
1524
+ const classification = classifyTransaction(legs, tx);
1279
1525
  return { tx, classification, legs };
1280
1526
  });
1281
1527
  const nonSpam = filterSpamTransactions(classified, spamConfig);
@@ -1287,20 +1533,38 @@ function createIndexer(options) {
1287
1533
  break;
1288
1534
  }
1289
1535
  }
1290
- return accumulated.slice(0, limit);
1536
+ const result = accumulated.slice(0, limit);
1537
+ return enrichBatch(result);
1291
1538
  },
1292
- async getTransaction(signature2, walletAddress) {
1539
+ async getTransaction(signature2, options2 = {}) {
1540
+ const { enrichNftMetadata = true } = options2;
1293
1541
  const tx = await fetchTransaction(client.rpc, signature2);
1294
1542
  if (!tx) {
1295
1543
  return null;
1296
1544
  }
1297
1545
  tx.protocol = detectProtocol(tx.programIds);
1298
- const legs = transactionToLegs(tx, walletAddress);
1299
- const classification = classifyTransaction(legs, walletAddress, tx);
1300
- return { tx, classification, legs };
1546
+ const legs = transactionToLegs(tx);
1547
+ const classification = classifyTransaction(legs, tx);
1548
+ let classified = { tx, classification, legs };
1549
+ if (enrichNftMetadata && rpcUrl) {
1550
+ classified = await enrichNftClassification(rpcUrl, classified);
1551
+ }
1552
+ return classified;
1301
1553
  },
1302
1554
  async getRawTransaction(signature2) {
1303
1555
  return fetchTransaction(client.rpc, signature2);
1556
+ },
1557
+ async getNftMetadata(mintAddress) {
1558
+ if (!rpcUrl) {
1559
+ throw new Error("getNftMetadata requires rpcUrl to be set");
1560
+ }
1561
+ return fetchNftMetadata(rpcUrl, mintAddress);
1562
+ },
1563
+ async getNftMetadataBatch(mintAddresses) {
1564
+ if (!rpcUrl) {
1565
+ throw new Error("getNftMetadataBatch requires rpcUrl to be set");
1566
+ }
1567
+ return fetchNftMetadataBatch(rpcUrl, mintAddresses);
1304
1568
  }
1305
1569
  };
1306
1570
  }
@@ -1352,6 +1616,6 @@ function groupLegsByToken(legs) {
1352
1616
  return grouped;
1353
1617
  }
1354
1618
 
1355
- 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 };
1619
+ 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, fetchNftMetadata, fetchNftMetadataBatch, fetchTransaction, fetchTransactionsBatch, fetchWalletBalance, fetchWalletSignatures, filterSpamTransactions, getTokenInfo, groupLegsByAccount, groupLegsByToken, isSolanaPayTransaction, isSpamTransaction, parseAccountId, parseAddress, parseSignature, parseSolanaPayMemo, transactionToLegs, validateLegsBalance };
1356
1620
  //# sourceMappingURL=index.js.map
1357
1621
  //# sourceMappingURL=index.js.map