tx-indexer 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
@@ -372,7 +382,7 @@ function isSolanaPayTransaction(programIds, memo) {
372
382
 
373
383
  // ../solana/src/fetcher/transactions.ts
374
384
  async function fetchWalletSignatures(rpc, walletAddress, config = {}) {
375
- const { limit = 1e3, before, until } = config;
385
+ const { limit = 100, before, until } = config;
376
386
  const response = await rpc.getSignaturesForAddress(walletAddress, {
377
387
  limit,
378
388
  before,
@@ -558,7 +568,7 @@ var DEX_PROTOCOL_IDS = /* @__PURE__ */ new Set([
558
568
  function isDexProtocol(protocol) {
559
569
  return protocol !== null && protocol !== void 0 && DEX_PROTOCOL_IDS.has(protocol.id);
560
570
  }
561
- function transactionToLegs(tx, walletAddress) {
571
+ function transactionToLegs(tx) {
562
572
  const legs = [];
563
573
  const feePayer = tx.accountKeys?.[0]?.toLowerCase();
564
574
  const solChanges = extractSolBalanceChanges(tx);
@@ -566,9 +576,8 @@ function transactionToLegs(tx, walletAddress) {
566
576
  let totalSolCredits = 0n;
567
577
  for (const change of solChanges) {
568
578
  if (change.change === 0n) continue;
569
- const isWallet = walletAddress ? change.address.toLowerCase() === walletAddress.toLowerCase() : false;
570
579
  const accountId = buildAccountId({
571
- type: isWallet ? "wallet" : "external",
580
+ type: "external",
572
581
  address: change.address
573
582
  });
574
583
  const solInfo = TOKEN_INFO[KNOWN_TOKENS.SOL];
@@ -588,7 +597,7 @@ function transactionToLegs(tx, walletAddress) {
588
597
  amountRaw: change.change.toString().replace("-", ""),
589
598
  amountUi: Math.abs(change.changeUi)
590
599
  },
591
- role: determineSolRole(change, walletAddress, tx, feePayer)
600
+ role: determineSolRole(change, tx, feePayer)
592
601
  });
593
602
  }
594
603
  const networkFee = totalSolDebits - totalSolCredits;
@@ -610,26 +619,22 @@ function transactionToLegs(tx, walletAddress) {
610
619
  const tokenChanges = extractTokenBalanceChanges(tx);
611
620
  for (const change of tokenChanges) {
612
621
  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
622
  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
- });
623
+ if (isDexProtocol(tx.protocol)) {
624
+ const isFeePayer = feePayer && change.owner?.toLowerCase() === feePayer;
625
+ if (isFeePayer) {
626
+ accountId = buildAccountId({
627
+ type: "external",
628
+ address: change.owner || feePayer
629
+ });
630
+ } else {
631
+ accountId = buildAccountId({
632
+ type: "protocol",
633
+ address: change.owner || change.tokenInfo.mint,
634
+ protocol: tx.protocol.id,
635
+ token: change.tokenInfo.symbol
636
+ });
637
+ }
633
638
  } else {
634
639
  accountId = buildAccountId({
635
640
  type: "external",
@@ -644,41 +649,30 @@ function transactionToLegs(tx, walletAddress) {
644
649
  amountRaw: change.change.raw.replace("-", ""),
645
650
  amountUi: Math.abs(change.change.ui)
646
651
  },
647
- role: determineTokenRole(change, walletAddress, tx, feePayer)
652
+ role: determineTokenRole(change, tx, feePayer)
648
653
  });
649
654
  }
650
655
  return legs;
651
656
  }
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;
657
+ function determineSolRole(change, tx, feePayer) {
658
+ const isFeePayer = feePayer ? change.address.toLowerCase() === feePayer : false;
655
659
  const isPositive = change.change > 0n;
656
660
  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
661
  if (isFeePayer && !isPositive && amountSol < 0.01) {
670
662
  return "fee";
671
663
  }
672
664
  if (isPositive) {
665
+ if (tx.protocol?.id === "stake") {
666
+ return "reward";
667
+ }
673
668
  return "received";
674
669
  }
675
670
  return "sent";
676
671
  }
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;
672
+ function determineTokenRole(change, tx, feePayer) {
673
+ const isFeePayer = feePayer ? change.owner?.toLowerCase() === feePayer : false;
680
674
  const isPositive = change.change.ui > 0;
681
- if (isWallet || isFeePayer) {
675
+ if (isFeePayer) {
682
676
  return isPositive ? "received" : "sent";
683
677
  }
684
678
  if (isDexProtocol(tx.protocol)) {
@@ -692,85 +686,35 @@ var TransferClassifier = class {
692
686
  name = "transfer";
693
687
  priority = 20;
694
688
  classify(context) {
695
- const { legs, walletAddress, tx } = context;
689
+ const { legs, tx } = context;
696
690
  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"
691
+ const senderLeg = legs.find(
692
+ (l) => l.accountId.startsWith("external:") && l.side === "debit" && l.role === "sent"
713
693
  );
714
- const participantReceived = legs.filter(
715
- (l) => l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.side === "credit" && l.role === "received"
694
+ if (!senderLeg) return null;
695
+ const sender = senderLeg.accountId.replace("external:", "");
696
+ const receiverLeg = legs.find(
697
+ (l) => l.accountId.startsWith("external:") && l.side === "credit" && l.role === "received" && l.amount.token.mint === senderLeg.amount.token.mint
716
698
  );
717
- const otherReceived = legs.filter(
718
- (l) => !l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.accountId.startsWith("external:") && l.side === "credit" && l.role === "received"
719
- );
720
- const otherSent = legs.filter(
721
- (l) => !l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.accountId.startsWith("external:") && l.side === "debit" && l.role === "sent"
722
- );
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
- };
699
+ if (!receiverLeg) return null;
700
+ const receiver = receiverLeg.accountId.replace("external:", "");
701
+ return {
702
+ primaryType: "transfer",
703
+ primaryAmount: senderLeg.amount,
704
+ secondaryAmount: null,
705
+ sender,
706
+ receiver,
707
+ counterparty: {
708
+ type: "wallet",
709
+ address: receiver,
710
+ name: `${receiver.slice(0, 8)}...`
711
+ },
712
+ confidence: 0.95,
713
+ isRelevant: true,
714
+ metadata: {
715
+ ...facilitator && { facilitator, payment_type: "facilitated" }
771
716
  }
772
- }
773
- return null;
717
+ };
774
718
  }
775
719
  };
776
720
 
@@ -815,9 +759,54 @@ var KNOWN_PROGRAMS = {
815
759
  [STAKE_PROGRAM_ID]: {
816
760
  id: "stake",
817
761
  name: "Stake Program"
762
+ },
763
+ [STAKE_POOL_PROGRAM_ID]: {
764
+ id: "stake-pool",
765
+ name: "Stake Pool Program"
766
+ },
767
+ [CANDY_GUARD_PROGRAM_ID]: {
768
+ id: "candy-guard",
769
+ name: "Metaplex Candy Guard Program"
770
+ },
771
+ [CANDY_MACHINE_V3_PROGRAM_ID]: {
772
+ id: "candy-machine-v3",
773
+ name: "Metaplex Candy Machine Core Program"
774
+ },
775
+ [BUBBLEGUM_PROGRAM_ID]: {
776
+ id: "bubblegum",
777
+ name: "Bubblegum Program"
778
+ },
779
+ [MAGIC_EDEN_CANDY_MACHINE_ID]: {
780
+ id: "magic-eden-candy-machine",
781
+ name: "Nft Candy Machine Program (Magic Eden)"
782
+ },
783
+ [WORMHOLE_PROGRAM_ID]: {
784
+ id: "wormhole",
785
+ name: "Wormhole"
786
+ },
787
+ [WORMHOLE_TOKEN_BRIDGE_ID]: {
788
+ id: "wormhole-token-bridge",
789
+ name: "Wormhole Token Bridge"
790
+ },
791
+ [DEGODS_BRIDGE_PROGRAM_ID]: {
792
+ id: "degods-bridge",
793
+ name: "DeGods Bridge"
794
+ },
795
+ [DEBRIDGE_PROGRAM_ID]: {
796
+ id: "debridge",
797
+ name: "deBridge"
798
+ },
799
+ [ALLBRIDGE_PROGRAM_ID]: {
800
+ id: "allbridge",
801
+ name: "Allbridge"
818
802
  }
819
803
  };
820
804
  var PRIORITY_ORDER = [
805
+ "wormhole",
806
+ "wormhole-token-bridge",
807
+ "degods-bridge",
808
+ "debridge",
809
+ "allbridge",
821
810
  "jupiter",
822
811
  "jupiter-v4",
823
812
  "raydium",
@@ -835,9 +824,33 @@ var DEX_PROTOCOL_IDS2 = /* @__PURE__ */ new Set([
835
824
  "raydium",
836
825
  "orca-whirlpool"
837
826
  ]);
827
+ var NFT_MINT_PROTOCOL_IDS = /* @__PURE__ */ new Set([
828
+ "metaplex",
829
+ "candy-machine-v3",
830
+ "candy-guard",
831
+ "bubblegum",
832
+ "magic-eden-candy-machine"
833
+ ]);
834
+ var STAKE_PROTOCOL_IDS = /* @__PURE__ */ new Set(["stake", "stake-pool"]);
835
+ var BRIDGE_PROTOCOL_IDS = /* @__PURE__ */ new Set([
836
+ "wormhole",
837
+ "wormhole-token-bridge",
838
+ "degods-bridge",
839
+ "debridge",
840
+ "allbridge"
841
+ ]);
838
842
  function isDexProtocolById(protocolId) {
839
843
  return protocolId !== void 0 && DEX_PROTOCOL_IDS2.has(protocolId);
840
844
  }
845
+ function isNftMintProtocolById(protocolId) {
846
+ return protocolId !== void 0 && NFT_MINT_PROTOCOL_IDS.has(protocolId);
847
+ }
848
+ function isStakeProtocolById(protocolId) {
849
+ return protocolId !== void 0 && STAKE_PROTOCOL_IDS.has(protocolId);
850
+ }
851
+ function isBridgeProtocolById(protocolId) {
852
+ return protocolId !== void 0 && BRIDGE_PROTOCOL_IDS.has(protocolId);
853
+ }
841
854
  function detectProtocol(programIds) {
842
855
  const detectedProtocols = [];
843
856
  for (const programId of programIds) {
@@ -862,65 +875,46 @@ var SwapClassifier = class {
862
875
  name = "swap";
863
876
  priority = 80;
864
877
  classify(context) {
865
- const { legs, walletAddress, tx } = context;
866
- const protocolLegs = legs.filter(
867
- (leg) => leg.accountId.startsWith("protocol:")
868
- );
878
+ const { legs, tx } = context;
869
879
  const hasDexProtocol = isDexProtocolById(tx.protocol?.id);
870
- if (protocolLegs.length === 0 && !hasDexProtocol) {
880
+ if (!hasDexProtocol) {
871
881
  return null;
872
882
  }
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"
883
+ const feeLeg = legs.find(
884
+ (leg) => leg.role === "fee" && leg.side === "debit"
890
885
  );
891
- const tokensOut = participantDebits.filter(
892
- (leg) => leg.role === "sent" || leg.role === "protocol_deposit"
886
+ const initiator = feeLeg?.accountId.replace("external:", "") ?? null;
887
+ const tokensOut = legs.filter(
888
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && (leg.role === "sent" || leg.role === "protocol_deposit")
893
889
  );
894
- const tokensIn = participantCredits.filter(
895
- (leg) => leg.role === "received" || leg.role === "protocol_withdraw"
890
+ const tokensIn = legs.filter(
891
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && (leg.role === "received" || leg.role === "protocol_withdraw")
896
892
  );
897
893
  if (tokensOut.length === 0 || tokensIn.length === 0) {
898
894
  return null;
899
895
  }
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
- }
896
+ const tokenOut = tokensOut[0];
897
+ const tokenIn = tokensIn[0];
898
+ if (tokenOut.amount.token.symbol === tokenIn.amount.token.symbol) {
899
+ return null;
922
900
  }
923
- return null;
901
+ return {
902
+ primaryType: "swap",
903
+ primaryAmount: tokenOut.amount,
904
+ secondaryAmount: tokenIn.amount,
905
+ sender: initiator,
906
+ receiver: initiator,
907
+ counterparty: null,
908
+ confidence: 0.9,
909
+ isRelevant: true,
910
+ metadata: {
911
+ swap_type: "token_to_token",
912
+ from_token: tokenOut.amount.token.symbol,
913
+ to_token: tokenIn.amount.token.symbol,
914
+ from_amount: tokenOut.amount.amountUi,
915
+ to_amount: tokenIn.amount.amountUi
916
+ }
917
+ };
924
918
  }
925
919
  };
926
920
 
@@ -929,7 +923,7 @@ var AirdropClassifier = class {
929
923
  name = "airdrop";
930
924
  priority = 70;
931
925
  classify(context) {
932
- const { legs, walletAddress, tx } = context;
926
+ const { legs, tx } = context;
933
927
  const facilitator = tx.accountKeys ? detectFacilitator(tx.accountKeys) : null;
934
928
  const protocolLegs = legs.filter(
935
929
  (leg) => leg.accountId.startsWith("protocol:")
@@ -937,49 +931,41 @@ var AirdropClassifier = class {
937
931
  if (protocolLegs.length === 0) {
938
932
  return null;
939
933
  }
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"
934
+ const tokenReceived = legs.filter(
935
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.role === "received" && leg.amount.token.symbol !== "SOL"
950
936
  );
951
937
  if (tokenReceived.length === 0) {
952
938
  return null;
953
939
  }
954
- const tokenSent = participantDebits.filter(
955
- (leg) => leg.role === "sent" && leg.amount.token.symbol !== "SOL"
940
+ const tokenSent = legs.filter(
941
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role === "sent" && leg.amount.token.symbol !== "SOL"
956
942
  );
957
943
  if (tokenSent.length > 0) {
958
944
  return null;
959
945
  }
960
946
  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)
947
+ const receiver = mainToken.accountId.replace("external:", "");
948
+ const senderLeg = legs.find(
949
+ (leg) => leg.side === "debit" && leg.amount.token.mint === mainToken.amount.token.mint
963
950
  );
964
- const sender = senderLegs.length > 0 ? senderLegs[0] : null;
965
- const receiverAddress = mainToken.accountId.replace(/^(external:|wallet:)/, "");
951
+ const sender = senderLeg ? senderLeg.accountId.replace(/^(external:|protocol:)/, "") : null;
966
952
  return {
967
953
  primaryType: "airdrop",
968
- direction: isObserverMode ? "neutral" : "incoming",
969
954
  primaryAmount: mainToken.amount,
970
955
  secondaryAmount: null,
956
+ sender,
957
+ receiver,
971
958
  counterparty: sender ? {
972
- type: "unknown",
973
- address: sender.accountId.replace(/^(external:|protocol:|wallet:)/, "")
959
+ type: "protocol",
960
+ address: sender
974
961
  } : null,
975
- confidence: isObserverMode ? 0.8 : 0.85,
962
+ confidence: 0.85,
976
963
  isRelevant: true,
977
964
  metadata: {
978
965
  airdrop_type: "token",
979
966
  token: mainToken.amount.token.symbol,
980
967
  amount: mainToken.amount.amountUi,
981
- ...isObserverMode && { observer_mode: true, receiver: receiverAddress },
982
- ...facilitator && { facilitator, payment_type: "facilitated" }
968
+ ...facilitator && { facilitator }
983
969
  }
984
970
  };
985
971
  }
@@ -990,30 +976,34 @@ var FeeOnlyClassifier = class {
990
976
  name = "fee-only";
991
977
  priority = 60;
992
978
  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) {
979
+ const { legs } = context;
980
+ const externalLegs = legs.filter(
981
+ (leg) => leg.accountId.startsWith("external:")
982
+ );
983
+ const nonFeeLegs = externalLegs.filter((leg) => leg.role !== "fee");
984
+ if (nonFeeLegs.length > 0) {
999
985
  return null;
1000
986
  }
1001
987
  const feeLegs = legs.filter((leg) => leg.role === "fee");
1002
988
  if (feeLegs.length === 0) {
1003
989
  return null;
1004
990
  }
991
+ const feePayerLeg = feeLegs.find(
992
+ (leg) => leg.side === "debit" && leg.amount.token.symbol === "SOL"
993
+ );
994
+ const feePayer = feePayerLeg?.accountId.replace("external:", "") ?? null;
1005
995
  const totalFee = feeLegs.find((leg) => leg.amount.token.symbol === "SOL");
1006
996
  return {
1007
997
  primaryType: "fee_only",
1008
- direction: "outgoing",
1009
998
  primaryAmount: totalFee?.amount ?? null,
1010
999
  secondaryAmount: null,
1000
+ sender: feePayer,
1001
+ receiver: null,
1011
1002
  counterparty: null,
1012
- confidence: isObserverMode ? 0.9 : 0.95,
1003
+ confidence: 0.95,
1013
1004
  isRelevant: false,
1014
1005
  metadata: {
1015
- fee_type: "network",
1016
- ...isObserverMode && { observer_mode: true }
1006
+ fee_type: "network"
1017
1007
  }
1018
1008
  };
1019
1009
  }
@@ -1024,59 +1014,31 @@ var SolanaPayClassifier = class {
1024
1014
  name = "solana-pay";
1025
1015
  priority = 95;
1026
1016
  classify(context) {
1027
- const { tx, walletAddress, legs } = context;
1017
+ const { tx, legs } = context;
1028
1018
  if (!isSolanaPayTransaction(tx.programIds, tx.memo)) {
1029
1019
  return null;
1030
1020
  }
1031
1021
  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"
1022
+ const senderLeg = legs.find(
1023
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role === "sent"
1068
1024
  );
1069
- const userReceived = legs.find(
1070
- (leg) => leg.accountId.includes(walletPrefix) && leg.side === "credit" && leg.role === "received"
1025
+ const receiverLeg = legs.find(
1026
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.role === "received"
1071
1027
  );
1072
- const direction = userSent ? "outgoing" : "incoming";
1073
- const primaryAmount = userSent?.amount ?? userReceived?.amount ?? null;
1028
+ const sender = senderLeg?.accountId.replace("external:", "") ?? null;
1029
+ const receiver = receiverLeg?.accountId.replace("external:", "") ?? null;
1030
+ const primaryAmount = senderLeg?.amount ?? receiverLeg?.amount ?? null;
1074
1031
  return {
1075
1032
  primaryType: "transfer",
1076
- direction,
1077
1033
  primaryAmount,
1078
1034
  secondaryAmount: null,
1079
- counterparty: memo.merchant ? { address: "", name: memo.merchant, type: "merchant" } : null,
1035
+ sender,
1036
+ receiver,
1037
+ counterparty: receiver ? {
1038
+ address: receiver,
1039
+ name: memo.merchant ?? void 0,
1040
+ type: memo.merchant ? "merchant" : "wallet"
1041
+ } : null,
1080
1042
  confidence: 0.98,
1081
1043
  isRelevant: true,
1082
1044
  metadata: {
@@ -1092,11 +1054,188 @@ var SolanaPayClassifier = class {
1092
1054
  }
1093
1055
  };
1094
1056
 
1057
+ // ../classification/src/classifiers/nft-mint-classifier.ts
1058
+ var NftMintClassifier = class {
1059
+ name = "nft-mint";
1060
+ priority = 85;
1061
+ classify(context) {
1062
+ const { legs, tx } = context;
1063
+ const hasNftMintProtocol = isNftMintProtocolById(tx.protocol?.id);
1064
+ if (!hasNftMintProtocol) {
1065
+ return null;
1066
+ }
1067
+ const nftCredits = legs.filter(
1068
+ (leg) => leg.side === "credit" && leg.amount.token.decimals === 0 && leg.amount.amountUi >= 1 && (leg.role === "received" || leg.role === "protocol_withdraw")
1069
+ );
1070
+ if (nftCredits.length === 0) {
1071
+ return null;
1072
+ }
1073
+ const primaryNft = nftCredits[0];
1074
+ const minter = primaryNft.accountId.replace("external:", "");
1075
+ const paymentLeg = legs.find(
1076
+ (leg) => leg.side === "debit" && leg.role === "sent" && leg.amount.token.symbol === "SOL"
1077
+ );
1078
+ const totalMinted = nftCredits.reduce(
1079
+ (sum, leg) => sum + leg.amount.amountUi,
1080
+ 0
1081
+ );
1082
+ return {
1083
+ primaryType: "nft_mint",
1084
+ primaryAmount: primaryNft.amount,
1085
+ secondaryAmount: paymentLeg?.amount ?? null,
1086
+ sender: null,
1087
+ receiver: minter,
1088
+ counterparty: null,
1089
+ confidence: 0.9,
1090
+ isRelevant: true,
1091
+ metadata: {
1092
+ nft_mint: primaryNft.amount.token.mint,
1093
+ nft_name: primaryNft.amount.token.name,
1094
+ quantity: totalMinted,
1095
+ mint_price: paymentLeg?.amount.amountUi,
1096
+ protocol: tx.protocol?.id
1097
+ }
1098
+ };
1099
+ }
1100
+ };
1101
+
1102
+ // ../classification/src/classifiers/stake-deposit-classifier.ts
1103
+ var StakeDepositClassifier = class {
1104
+ name = "stake-deposit";
1105
+ priority = 82;
1106
+ classify(context) {
1107
+ const { legs, tx } = context;
1108
+ const hasStakeProtocol = isStakeProtocolById(tx.protocol?.id);
1109
+ if (!hasStakeProtocol) {
1110
+ return null;
1111
+ }
1112
+ const solDebit = legs.find(
1113
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.amount.token.symbol === "SOL" && (leg.role === "sent" || leg.role === "protocol_deposit")
1114
+ );
1115
+ if (!solDebit) {
1116
+ return null;
1117
+ }
1118
+ const staker = solDebit.accountId.replace("external:", "");
1119
+ return {
1120
+ primaryType: "stake_deposit",
1121
+ primaryAmount: solDebit.amount,
1122
+ secondaryAmount: null,
1123
+ sender: staker,
1124
+ receiver: null,
1125
+ counterparty: null,
1126
+ confidence: 0.9,
1127
+ isRelevant: true,
1128
+ metadata: {
1129
+ stake_amount: solDebit.amount.amountUi,
1130
+ protocol: tx.protocol?.id
1131
+ }
1132
+ };
1133
+ }
1134
+ };
1135
+
1136
+ // ../classification/src/classifiers/stake-withdraw-classifier.ts
1137
+ var StakeWithdrawClassifier = class {
1138
+ name = "stake-withdraw";
1139
+ priority = 81;
1140
+ classify(context) {
1141
+ const { legs, tx } = context;
1142
+ const hasStakeProtocol = isStakeProtocolById(tx.protocol?.id);
1143
+ if (!hasStakeProtocol) {
1144
+ return null;
1145
+ }
1146
+ const solCredit = legs.find(
1147
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.amount.token.symbol === "SOL" && (leg.role === "received" || leg.role === "protocol_withdraw")
1148
+ );
1149
+ if (!solCredit) {
1150
+ return null;
1151
+ }
1152
+ const solDebit = legs.find(
1153
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.amount.token.symbol === "SOL" && (leg.role === "sent" || leg.role === "protocol_deposit")
1154
+ );
1155
+ if (solDebit) {
1156
+ return null;
1157
+ }
1158
+ const withdrawer = solCredit.accountId.replace("external:", "");
1159
+ return {
1160
+ primaryType: "stake_withdraw",
1161
+ primaryAmount: solCredit.amount,
1162
+ secondaryAmount: null,
1163
+ sender: null,
1164
+ receiver: withdrawer,
1165
+ counterparty: null,
1166
+ confidence: 0.9,
1167
+ isRelevant: true,
1168
+ metadata: {
1169
+ withdraw_amount: solCredit.amount.amountUi,
1170
+ protocol: tx.protocol?.id
1171
+ }
1172
+ };
1173
+ }
1174
+ };
1175
+
1176
+ // ../classification/src/classifiers/bridge-classifier.ts
1177
+ var BridgeClassifier = class {
1178
+ name = "bridge";
1179
+ priority = 88;
1180
+ classify(context) {
1181
+ const { legs, tx } = context;
1182
+ const hasBridgeProtocol = isBridgeProtocolById(tx.protocol?.id);
1183
+ if (!hasBridgeProtocol) {
1184
+ return null;
1185
+ }
1186
+ const tokensOut = legs.filter(
1187
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role !== "fee" && (leg.role === "sent" || leg.role === "protocol_deposit")
1188
+ );
1189
+ const tokensIn = legs.filter(
1190
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && (leg.role === "received" || leg.role === "protocol_withdraw")
1191
+ );
1192
+ let primaryType;
1193
+ let primaryAmount = null;
1194
+ let participant = null;
1195
+ if (tokensIn.length > 0 && tokensOut.length === 0) {
1196
+ primaryType = "bridge_in";
1197
+ const creditLeg = tokensIn[0];
1198
+ primaryAmount = creditLeg.amount;
1199
+ participant = creditLeg.accountId.replace("external:", "");
1200
+ } else if (tokensOut.length > 0 && tokensIn.length === 0) {
1201
+ primaryType = "bridge_out";
1202
+ const debitLeg = tokensOut[0];
1203
+ primaryAmount = debitLeg.amount;
1204
+ participant = debitLeg.accountId.replace("external:", "");
1205
+ } else if (tokensIn.length > 0 && tokensOut.length > 0) {
1206
+ primaryType = "bridge_in";
1207
+ const creditLeg = tokensIn[0];
1208
+ primaryAmount = creditLeg.amount;
1209
+ participant = creditLeg.accountId.replace("external:", "");
1210
+ } else {
1211
+ return null;
1212
+ }
1213
+ return {
1214
+ primaryType,
1215
+ primaryAmount,
1216
+ secondaryAmount: null,
1217
+ sender: primaryType === "bridge_out" ? participant : null,
1218
+ receiver: primaryType === "bridge_in" ? participant : null,
1219
+ counterparty: null,
1220
+ confidence: 0.9,
1221
+ isRelevant: true,
1222
+ metadata: {
1223
+ bridge_protocol: tx.protocol?.id,
1224
+ bridge_name: tx.protocol?.name
1225
+ }
1226
+ };
1227
+ }
1228
+ };
1229
+
1095
1230
  // ../classification/src/engine/classification-service.ts
1096
1231
  var ClassificationService = class {
1097
1232
  classifiers = [];
1098
1233
  constructor() {
1099
1234
  this.registerClassifier(new SolanaPayClassifier());
1235
+ this.registerClassifier(new BridgeClassifier());
1236
+ this.registerClassifier(new NftMintClassifier());
1237
+ this.registerClassifier(new StakeDepositClassifier());
1238
+ this.registerClassifier(new StakeWithdrawClassifier());
1100
1239
  this.registerClassifier(new SwapClassifier());
1101
1240
  this.registerClassifier(new AirdropClassifier());
1102
1241
  this.registerClassifier(new TransferClassifier());
@@ -1106,8 +1245,8 @@ var ClassificationService = class {
1106
1245
  this.classifiers.push(classifier);
1107
1246
  this.classifiers.sort((a, b) => b.priority - a.priority);
1108
1247
  }
1109
- classify(legs, walletAddress, tx) {
1110
- const context = { legs, walletAddress, tx };
1248
+ classify(legs, tx) {
1249
+ const context = { legs, tx };
1111
1250
  for (const classifier of this.classifiers) {
1112
1251
  const result = classifier.classify(context);
1113
1252
  if (result && result.isRelevant) {
@@ -1116,9 +1255,10 @@ var ClassificationService = class {
1116
1255
  }
1117
1256
  return {
1118
1257
  primaryType: "other",
1119
- direction: "neutral",
1120
1258
  primaryAmount: null,
1121
1259
  secondaryAmount: null,
1260
+ sender: null,
1261
+ receiver: null,
1122
1262
  counterparty: null,
1123
1263
  confidence: 0,
1124
1264
  isRelevant: false,
@@ -1127,8 +1267,8 @@ var ClassificationService = class {
1127
1267
  }
1128
1268
  };
1129
1269
  var classificationService = new ClassificationService();
1130
- function classifyTransaction(legs, walletAddress, tx) {
1131
- return classificationService.classify(legs, walletAddress, tx);
1270
+ function classifyTransaction(legs, tx) {
1271
+ return classificationService.classify(legs, tx);
1132
1272
  }
1133
1273
 
1134
1274
  // ../domain/src/tx/spam-filter.ts
@@ -1202,8 +1342,8 @@ function createIndexer(options) {
1202
1342
  );
1203
1343
  const classified = transactions.map((tx) => {
1204
1344
  tx.protocol = detectProtocol(tx.programIds);
1205
- const legs = transactionToLegs(tx, walletAddress);
1206
- const classification = classifyTransaction(legs, walletAddress, tx);
1345
+ const legs = transactionToLegs(tx);
1346
+ const classification = classifyTransaction(legs, tx);
1207
1347
  return { tx, classification, legs };
1208
1348
  });
1209
1349
  return classified;
@@ -1232,8 +1372,8 @@ function createIndexer(options) {
1232
1372
  );
1233
1373
  const classified = transactions.map((tx) => {
1234
1374
  tx.protocol = detectProtocol(tx.programIds);
1235
- const legs = transactionToLegs(tx, walletAddress);
1236
- const classification = classifyTransaction(legs, walletAddress, tx);
1375
+ const legs = transactionToLegs(tx);
1376
+ const classification = classifyTransaction(legs, tx);
1237
1377
  return { tx, classification, legs };
1238
1378
  });
1239
1379
  const nonSpam = filterSpamTransactions(classified, spamConfig);
@@ -1247,14 +1387,14 @@ function createIndexer(options) {
1247
1387
  }
1248
1388
  return accumulated.slice(0, limit);
1249
1389
  },
1250
- async getTransaction(signature2, walletAddress) {
1390
+ async getTransaction(signature2) {
1251
1391
  const tx = await fetchTransaction(client.rpc, signature2);
1252
1392
  if (!tx) {
1253
1393
  return null;
1254
1394
  }
1255
1395
  tx.protocol = detectProtocol(tx.programIds);
1256
- const legs = transactionToLegs(tx, walletAddress);
1257
- const classification = classifyTransaction(legs, walletAddress, tx);
1396
+ const legs = transactionToLegs(tx);
1397
+ const classification = classifyTransaction(legs, tx);
1258
1398
  return { tx, classification, legs };
1259
1399
  },
1260
1400
  async getRawTransaction(signature2) {