tx-indexer 0.3.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/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
@@ -600,7 +610,7 @@ var DEX_PROTOCOL_IDS = /* @__PURE__ */ new Set([
600
610
  function isDexProtocol(protocol) {
601
611
  return protocol !== null && protocol !== void 0 && DEX_PROTOCOL_IDS.has(protocol.id);
602
612
  }
603
- function transactionToLegs(tx, walletAddress) {
613
+ function transactionToLegs(tx) {
604
614
  const legs = [];
605
615
  const feePayer = tx.accountKeys?.[0]?.toLowerCase();
606
616
  const solChanges = extractSolBalanceChanges(tx);
@@ -608,9 +618,8 @@ function transactionToLegs(tx, walletAddress) {
608
618
  let totalSolCredits = 0n;
609
619
  for (const change of solChanges) {
610
620
  if (change.change === 0n) continue;
611
- const isWallet = walletAddress ? change.address.toLowerCase() === walletAddress.toLowerCase() : false;
612
621
  const accountId = buildAccountId({
613
- type: isWallet ? "wallet" : "external",
622
+ type: "external",
614
623
  address: change.address
615
624
  });
616
625
  const solInfo = TOKEN_INFO[KNOWN_TOKENS.SOL];
@@ -630,7 +639,7 @@ function transactionToLegs(tx, walletAddress) {
630
639
  amountRaw: change.change.toString().replace("-", ""),
631
640
  amountUi: Math.abs(change.changeUi)
632
641
  },
633
- role: determineSolRole(change, walletAddress, tx, feePayer)
642
+ role: determineSolRole(change, tx, feePayer)
634
643
  });
635
644
  }
636
645
  const networkFee = totalSolDebits - totalSolCredits;
@@ -652,26 +661,22 @@ function transactionToLegs(tx, walletAddress) {
652
661
  const tokenChanges = extractTokenBalanceChanges(tx);
653
662
  for (const change of tokenChanges) {
654
663
  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
664
  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
- });
665
+ if (isDexProtocol(tx.protocol)) {
666
+ const isFeePayer = feePayer && change.owner?.toLowerCase() === feePayer;
667
+ if (isFeePayer) {
668
+ accountId = buildAccountId({
669
+ type: "external",
670
+ address: change.owner || feePayer
671
+ });
672
+ } else {
673
+ accountId = buildAccountId({
674
+ type: "protocol",
675
+ address: change.owner || change.tokenInfo.mint,
676
+ protocol: tx.protocol.id,
677
+ token: change.tokenInfo.symbol
678
+ });
679
+ }
675
680
  } else {
676
681
  accountId = buildAccountId({
677
682
  type: "external",
@@ -686,41 +691,30 @@ function transactionToLegs(tx, walletAddress) {
686
691
  amountRaw: change.change.raw.replace("-", ""),
687
692
  amountUi: Math.abs(change.change.ui)
688
693
  },
689
- role: determineTokenRole(change, walletAddress, tx, feePayer)
694
+ role: determineTokenRole(change, tx, feePayer)
690
695
  });
691
696
  }
692
697
  return legs;
693
698
  }
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;
699
+ function determineSolRole(change, tx, feePayer) {
700
+ const isFeePayer = feePayer ? change.address.toLowerCase() === feePayer : false;
697
701
  const isPositive = change.change > 0n;
698
702
  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
703
  if (isFeePayer && !isPositive && amountSol < 0.01) {
712
704
  return "fee";
713
705
  }
714
706
  if (isPositive) {
707
+ if (tx.protocol?.id === "stake") {
708
+ return "reward";
709
+ }
715
710
  return "received";
716
711
  }
717
712
  return "sent";
718
713
  }
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;
714
+ function determineTokenRole(change, tx, feePayer) {
715
+ const isFeePayer = feePayer ? change.owner?.toLowerCase() === feePayer : false;
722
716
  const isPositive = change.change.ui > 0;
723
- if (isWallet || isFeePayer) {
717
+ if (isFeePayer) {
724
718
  return isPositive ? "received" : "sent";
725
719
  }
726
720
  if (isDexProtocol(tx.protocol)) {
@@ -734,85 +728,35 @@ var TransferClassifier = class {
734
728
  name = "transfer";
735
729
  priority = 20;
736
730
  classify(context) {
737
- const { legs, walletAddress, tx } = context;
731
+ const { legs, tx } = context;
738
732
  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"
733
+ const senderLeg = legs.find(
734
+ (l) => l.accountId.startsWith("external:") && l.side === "debit" && l.role === "sent"
755
735
  );
756
- const participantReceived = legs.filter(
757
- (l) => l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.side === "credit" && l.role === "received"
736
+ if (!senderLeg) return null;
737
+ const sender = senderLeg.accountId.replace("external:", "");
738
+ const receiverLeg = legs.find(
739
+ (l) => l.accountId.startsWith("external:") && l.side === "credit" && l.role === "received" && l.amount.token.mint === senderLeg.amount.token.mint
758
740
  );
759
- const otherReceived = legs.filter(
760
- (l) => !l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.accountId.startsWith("external:") && l.side === "credit" && l.role === "received"
761
- );
762
- const otherSent = legs.filter(
763
- (l) => !l.accountId.toLowerCase().startsWith(participantPrefix.toLowerCase()) && l.accountId.startsWith("external:") && l.side === "debit" && l.role === "sent"
764
- );
765
- for (const sent of participantSent) {
766
- const matchingReceived = otherReceived.find(
767
- (r) => r.amount.token.mint === sent.amount.token.mint
768
- );
769
- if (matchingReceived) {
770
- const receiverAddress = matchingReceived.accountId.replace("external:", "");
771
- return {
772
- primaryType: "transfer",
773
- direction: isObserverMode ? "neutral" : "outgoing",
774
- primaryAmount: sent.amount,
775
- secondaryAmount: null,
776
- counterparty: {
777
- type: "unknown",
778
- address: receiverAddress,
779
- name: `${receiverAddress.slice(0, 8)}...`
780
- },
781
- confidence: isObserverMode ? 0.9 : 0.95,
782
- isRelevant: true,
783
- metadata: {
784
- ...isObserverMode && { observer_mode: true, sender: participantAddress },
785
- ...facilitator && { facilitator, payment_type: "facilitated" }
786
- }
787
- };
788
- }
789
- }
790
- for (const received of participantReceived) {
791
- const matchingSent = otherSent.find(
792
- (s) => s.amount.token.mint === received.amount.token.mint
793
- );
794
- if (matchingSent) {
795
- const senderAddress = matchingSent.accountId.replace("external:", "");
796
- return {
797
- primaryType: "transfer",
798
- direction: isObserverMode ? "neutral" : "incoming",
799
- primaryAmount: received.amount,
800
- secondaryAmount: null,
801
- counterparty: {
802
- type: "unknown",
803
- address: senderAddress,
804
- name: `${senderAddress.slice(0, 8)}...`
805
- },
806
- confidence: isObserverMode ? 0.9 : 0.95,
807
- isRelevant: true,
808
- metadata: {
809
- ...isObserverMode && { observer_mode: true, receiver: participantAddress },
810
- ...facilitator && { facilitator, payment_type: "facilitated" }
811
- }
812
- };
741
+ if (!receiverLeg) return null;
742
+ const receiver = receiverLeg.accountId.replace("external:", "");
743
+ return {
744
+ primaryType: "transfer",
745
+ primaryAmount: senderLeg.amount,
746
+ secondaryAmount: null,
747
+ sender,
748
+ receiver,
749
+ counterparty: {
750
+ type: "wallet",
751
+ address: receiver,
752
+ name: `${receiver.slice(0, 8)}...`
753
+ },
754
+ confidence: 0.95,
755
+ isRelevant: true,
756
+ metadata: {
757
+ ...facilitator && { facilitator, payment_type: "facilitated" }
813
758
  }
814
- }
815
- return null;
759
+ };
816
760
  }
817
761
  };
818
762
 
@@ -857,9 +801,54 @@ var KNOWN_PROGRAMS = {
857
801
  [STAKE_PROGRAM_ID]: {
858
802
  id: "stake",
859
803
  name: "Stake Program"
804
+ },
805
+ [STAKE_POOL_PROGRAM_ID]: {
806
+ id: "stake-pool",
807
+ name: "Stake Pool Program"
808
+ },
809
+ [CANDY_GUARD_PROGRAM_ID]: {
810
+ id: "candy-guard",
811
+ name: "Metaplex Candy Guard Program"
812
+ },
813
+ [CANDY_MACHINE_V3_PROGRAM_ID]: {
814
+ id: "candy-machine-v3",
815
+ name: "Metaplex Candy Machine Core Program"
816
+ },
817
+ [BUBBLEGUM_PROGRAM_ID]: {
818
+ id: "bubblegum",
819
+ name: "Bubblegum Program"
820
+ },
821
+ [MAGIC_EDEN_CANDY_MACHINE_ID]: {
822
+ id: "magic-eden-candy-machine",
823
+ name: "Nft Candy Machine Program (Magic Eden)"
824
+ },
825
+ [WORMHOLE_PROGRAM_ID]: {
826
+ id: "wormhole",
827
+ name: "Wormhole"
828
+ },
829
+ [WORMHOLE_TOKEN_BRIDGE_ID]: {
830
+ id: "wormhole-token-bridge",
831
+ name: "Wormhole Token Bridge"
832
+ },
833
+ [DEGODS_BRIDGE_PROGRAM_ID]: {
834
+ id: "degods-bridge",
835
+ name: "DeGods Bridge"
836
+ },
837
+ [DEBRIDGE_PROGRAM_ID]: {
838
+ id: "debridge",
839
+ name: "deBridge"
840
+ },
841
+ [ALLBRIDGE_PROGRAM_ID]: {
842
+ id: "allbridge",
843
+ name: "Allbridge"
860
844
  }
861
845
  };
862
846
  var PRIORITY_ORDER = [
847
+ "wormhole",
848
+ "wormhole-token-bridge",
849
+ "degods-bridge",
850
+ "debridge",
851
+ "allbridge",
863
852
  "jupiter",
864
853
  "jupiter-v4",
865
854
  "raydium",
@@ -877,9 +866,33 @@ var DEX_PROTOCOL_IDS2 = /* @__PURE__ */ new Set([
877
866
  "raydium",
878
867
  "orca-whirlpool"
879
868
  ]);
869
+ var NFT_MINT_PROTOCOL_IDS = /* @__PURE__ */ new Set([
870
+ "metaplex",
871
+ "candy-machine-v3",
872
+ "candy-guard",
873
+ "bubblegum",
874
+ "magic-eden-candy-machine"
875
+ ]);
876
+ var STAKE_PROTOCOL_IDS = /* @__PURE__ */ new Set(["stake", "stake-pool"]);
877
+ var BRIDGE_PROTOCOL_IDS = /* @__PURE__ */ new Set([
878
+ "wormhole",
879
+ "wormhole-token-bridge",
880
+ "degods-bridge",
881
+ "debridge",
882
+ "allbridge"
883
+ ]);
880
884
  function isDexProtocolById(protocolId) {
881
885
  return protocolId !== void 0 && DEX_PROTOCOL_IDS2.has(protocolId);
882
886
  }
887
+ function isNftMintProtocolById(protocolId) {
888
+ return protocolId !== void 0 && NFT_MINT_PROTOCOL_IDS.has(protocolId);
889
+ }
890
+ function isStakeProtocolById(protocolId) {
891
+ return protocolId !== void 0 && STAKE_PROTOCOL_IDS.has(protocolId);
892
+ }
893
+ function isBridgeProtocolById(protocolId) {
894
+ return protocolId !== void 0 && BRIDGE_PROTOCOL_IDS.has(protocolId);
895
+ }
883
896
  function detectProtocol(programIds) {
884
897
  const detectedProtocols = [];
885
898
  for (const programId of programIds) {
@@ -904,65 +917,46 @@ var SwapClassifier = class {
904
917
  name = "swap";
905
918
  priority = 80;
906
919
  classify(context) {
907
- const { legs, walletAddress, tx } = context;
908
- const protocolLegs = legs.filter(
909
- (leg) => leg.accountId.startsWith("protocol:")
910
- );
920
+ const { legs, tx } = context;
911
921
  const hasDexProtocol = isDexProtocolById(tx.protocol?.id);
912
- if (protocolLegs.length === 0 && !hasDexProtocol) {
922
+ if (!hasDexProtocol) {
913
923
  return null;
914
924
  }
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"
925
+ const feeLeg = legs.find(
926
+ (leg) => leg.role === "fee" && leg.side === "debit"
929
927
  );
930
- const participantCredits = legs.filter(
931
- (leg) => leg.accountId.startsWith(participantPrefix) && leg.side === "credit"
928
+ const initiator = feeLeg?.accountId.replace("external:", "") ?? null;
929
+ const tokensOut = legs.filter(
930
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && (leg.role === "sent" || leg.role === "protocol_deposit")
932
931
  );
933
- const tokensOut = participantDebits.filter(
934
- (leg) => leg.role === "sent" || leg.role === "protocol_deposit"
935
- );
936
- const tokensIn = participantCredits.filter(
937
- (leg) => leg.role === "received" || leg.role === "protocol_withdraw"
932
+ const tokensIn = legs.filter(
933
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && (leg.role === "received" || leg.role === "protocol_withdraw")
938
934
  );
939
935
  if (tokensOut.length === 0 || tokensIn.length === 0) {
940
936
  return null;
941
937
  }
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
- }
938
+ const tokenOut = tokensOut[0];
939
+ const tokenIn = tokensIn[0];
940
+ if (tokenOut.amount.token.symbol === tokenIn.amount.token.symbol) {
941
+ return null;
964
942
  }
965
- return null;
943
+ return {
944
+ primaryType: "swap",
945
+ primaryAmount: tokenOut.amount,
946
+ secondaryAmount: tokenIn.amount,
947
+ sender: initiator,
948
+ receiver: initiator,
949
+ counterparty: null,
950
+ confidence: 0.9,
951
+ isRelevant: true,
952
+ metadata: {
953
+ swap_type: "token_to_token",
954
+ from_token: tokenOut.amount.token.symbol,
955
+ to_token: tokenIn.amount.token.symbol,
956
+ from_amount: tokenOut.amount.amountUi,
957
+ to_amount: tokenIn.amount.amountUi
958
+ }
959
+ };
966
960
  }
967
961
  };
968
962
 
@@ -971,7 +965,7 @@ var AirdropClassifier = class {
971
965
  name = "airdrop";
972
966
  priority = 70;
973
967
  classify(context) {
974
- const { legs, walletAddress, tx } = context;
968
+ const { legs, tx } = context;
975
969
  const facilitator = tx.accountKeys ? detectFacilitator(tx.accountKeys) : null;
976
970
  const protocolLegs = legs.filter(
977
971
  (leg) => leg.accountId.startsWith("protocol:")
@@ -979,49 +973,41 @@ var AirdropClassifier = class {
979
973
  if (protocolLegs.length === 0) {
980
974
  return null;
981
975
  }
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"
976
+ const tokenReceived = legs.filter(
977
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.role === "received" && leg.amount.token.symbol !== "SOL"
992
978
  );
993
979
  if (tokenReceived.length === 0) {
994
980
  return null;
995
981
  }
996
- const tokenSent = participantDebits.filter(
997
- (leg) => leg.role === "sent" && leg.amount.token.symbol !== "SOL"
982
+ const tokenSent = legs.filter(
983
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role === "sent" && leg.amount.token.symbol !== "SOL"
998
984
  );
999
985
  if (tokenSent.length > 0) {
1000
986
  return null;
1001
987
  }
1002
988
  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)
989
+ const receiver = mainToken.accountId.replace("external:", "");
990
+ const senderLeg = legs.find(
991
+ (leg) => leg.side === "debit" && leg.amount.token.mint === mainToken.amount.token.mint
1005
992
  );
1006
- const sender = senderLegs.length > 0 ? senderLegs[0] : null;
1007
- const receiverAddress = mainToken.accountId.replace(/^(external:|wallet:)/, "");
993
+ const sender = senderLeg ? senderLeg.accountId.replace(/^(external:|protocol:)/, "") : null;
1008
994
  return {
1009
995
  primaryType: "airdrop",
1010
- direction: isObserverMode ? "neutral" : "incoming",
1011
996
  primaryAmount: mainToken.amount,
1012
997
  secondaryAmount: null,
998
+ sender,
999
+ receiver,
1013
1000
  counterparty: sender ? {
1014
- type: "unknown",
1015
- address: sender.accountId.replace(/^(external:|protocol:|wallet:)/, "")
1001
+ type: "protocol",
1002
+ address: sender
1016
1003
  } : null,
1017
- confidence: isObserverMode ? 0.8 : 0.85,
1004
+ confidence: 0.85,
1018
1005
  isRelevant: true,
1019
1006
  metadata: {
1020
1007
  airdrop_type: "token",
1021
1008
  token: mainToken.amount.token.symbol,
1022
1009
  amount: mainToken.amount.amountUi,
1023
- ...isObserverMode && { observer_mode: true, receiver: receiverAddress },
1024
- ...facilitator && { facilitator, payment_type: "facilitated" }
1010
+ ...facilitator && { facilitator }
1025
1011
  }
1026
1012
  };
1027
1013
  }
@@ -1032,30 +1018,34 @@ var FeeOnlyClassifier = class {
1032
1018
  name = "fee-only";
1033
1019
  priority = 60;
1034
1020
  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) {
1021
+ const { legs } = context;
1022
+ const externalLegs = legs.filter(
1023
+ (leg) => leg.accountId.startsWith("external:")
1024
+ );
1025
+ const nonFeeLegs = externalLegs.filter((leg) => leg.role !== "fee");
1026
+ if (nonFeeLegs.length > 0) {
1041
1027
  return null;
1042
1028
  }
1043
1029
  const feeLegs = legs.filter((leg) => leg.role === "fee");
1044
1030
  if (feeLegs.length === 0) {
1045
1031
  return null;
1046
1032
  }
1033
+ const feePayerLeg = feeLegs.find(
1034
+ (leg) => leg.side === "debit" && leg.amount.token.symbol === "SOL"
1035
+ );
1036
+ const feePayer = feePayerLeg?.accountId.replace("external:", "") ?? null;
1047
1037
  const totalFee = feeLegs.find((leg) => leg.amount.token.symbol === "SOL");
1048
1038
  return {
1049
1039
  primaryType: "fee_only",
1050
- direction: "outgoing",
1051
1040
  primaryAmount: totalFee?.amount ?? null,
1052
1041
  secondaryAmount: null,
1042
+ sender: feePayer,
1043
+ receiver: null,
1053
1044
  counterparty: null,
1054
- confidence: isObserverMode ? 0.9 : 0.95,
1045
+ confidence: 0.95,
1055
1046
  isRelevant: false,
1056
1047
  metadata: {
1057
- fee_type: "network",
1058
- ...isObserverMode && { observer_mode: true }
1048
+ fee_type: "network"
1059
1049
  }
1060
1050
  };
1061
1051
  }
@@ -1066,59 +1056,31 @@ var SolanaPayClassifier = class {
1066
1056
  name = "solana-pay";
1067
1057
  priority = 95;
1068
1058
  classify(context) {
1069
- const { tx, walletAddress, legs } = context;
1059
+ const { tx, legs } = context;
1070
1060
  if (!isSolanaPayTransaction(tx.programIds, tx.memo)) {
1071
1061
  return null;
1072
1062
  }
1073
1063
  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"
1064
+ const senderLeg = legs.find(
1065
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role === "sent"
1110
1066
  );
1111
- const userReceived = legs.find(
1112
- (leg) => leg.accountId.includes(walletPrefix) && leg.side === "credit" && leg.role === "received"
1067
+ const receiverLeg = legs.find(
1068
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.role === "received"
1113
1069
  );
1114
- const direction = userSent ? "outgoing" : "incoming";
1115
- const primaryAmount = userSent?.amount ?? userReceived?.amount ?? null;
1070
+ const sender = senderLeg?.accountId.replace("external:", "") ?? null;
1071
+ const receiver = receiverLeg?.accountId.replace("external:", "") ?? null;
1072
+ const primaryAmount = senderLeg?.amount ?? receiverLeg?.amount ?? null;
1116
1073
  return {
1117
1074
  primaryType: "transfer",
1118
- direction,
1119
1075
  primaryAmount,
1120
1076
  secondaryAmount: null,
1121
- counterparty: memo.merchant ? { address: "", name: memo.merchant, type: "merchant" } : null,
1077
+ sender,
1078
+ receiver,
1079
+ counterparty: receiver ? {
1080
+ address: receiver,
1081
+ name: memo.merchant ?? void 0,
1082
+ type: memo.merchant ? "merchant" : "wallet"
1083
+ } : null,
1122
1084
  confidence: 0.98,
1123
1085
  isRelevant: true,
1124
1086
  metadata: {
@@ -1134,11 +1096,188 @@ var SolanaPayClassifier = class {
1134
1096
  }
1135
1097
  };
1136
1098
 
1099
+ // ../classification/src/classifiers/nft-mint-classifier.ts
1100
+ var NftMintClassifier = class {
1101
+ name = "nft-mint";
1102
+ priority = 85;
1103
+ classify(context) {
1104
+ const { legs, tx } = context;
1105
+ const hasNftMintProtocol = isNftMintProtocolById(tx.protocol?.id);
1106
+ if (!hasNftMintProtocol) {
1107
+ return null;
1108
+ }
1109
+ const nftCredits = legs.filter(
1110
+ (leg) => leg.side === "credit" && leg.amount.token.decimals === 0 && leg.amount.amountUi >= 1 && (leg.role === "received" || leg.role === "protocol_withdraw")
1111
+ );
1112
+ if (nftCredits.length === 0) {
1113
+ return null;
1114
+ }
1115
+ const primaryNft = nftCredits[0];
1116
+ const minter = primaryNft.accountId.replace("external:", "");
1117
+ const paymentLeg = legs.find(
1118
+ (leg) => leg.side === "debit" && leg.role === "sent" && leg.amount.token.symbol === "SOL"
1119
+ );
1120
+ const totalMinted = nftCredits.reduce(
1121
+ (sum, leg) => sum + leg.amount.amountUi,
1122
+ 0
1123
+ );
1124
+ return {
1125
+ primaryType: "nft_mint",
1126
+ primaryAmount: primaryNft.amount,
1127
+ secondaryAmount: paymentLeg?.amount ?? null,
1128
+ sender: null,
1129
+ receiver: minter,
1130
+ counterparty: null,
1131
+ confidence: 0.9,
1132
+ isRelevant: true,
1133
+ metadata: {
1134
+ nft_mint: primaryNft.amount.token.mint,
1135
+ nft_name: primaryNft.amount.token.name,
1136
+ quantity: totalMinted,
1137
+ mint_price: paymentLeg?.amount.amountUi,
1138
+ protocol: tx.protocol?.id
1139
+ }
1140
+ };
1141
+ }
1142
+ };
1143
+
1144
+ // ../classification/src/classifiers/stake-deposit-classifier.ts
1145
+ var StakeDepositClassifier = class {
1146
+ name = "stake-deposit";
1147
+ priority = 82;
1148
+ classify(context) {
1149
+ const { legs, tx } = context;
1150
+ const hasStakeProtocol = isStakeProtocolById(tx.protocol?.id);
1151
+ if (!hasStakeProtocol) {
1152
+ return null;
1153
+ }
1154
+ const solDebit = legs.find(
1155
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.amount.token.symbol === "SOL" && (leg.role === "sent" || leg.role === "protocol_deposit")
1156
+ );
1157
+ if (!solDebit) {
1158
+ return null;
1159
+ }
1160
+ const staker = solDebit.accountId.replace("external:", "");
1161
+ return {
1162
+ primaryType: "stake_deposit",
1163
+ primaryAmount: solDebit.amount,
1164
+ secondaryAmount: null,
1165
+ sender: staker,
1166
+ receiver: null,
1167
+ counterparty: null,
1168
+ confidence: 0.9,
1169
+ isRelevant: true,
1170
+ metadata: {
1171
+ stake_amount: solDebit.amount.amountUi,
1172
+ protocol: tx.protocol?.id
1173
+ }
1174
+ };
1175
+ }
1176
+ };
1177
+
1178
+ // ../classification/src/classifiers/stake-withdraw-classifier.ts
1179
+ var StakeWithdrawClassifier = class {
1180
+ name = "stake-withdraw";
1181
+ priority = 81;
1182
+ classify(context) {
1183
+ const { legs, tx } = context;
1184
+ const hasStakeProtocol = isStakeProtocolById(tx.protocol?.id);
1185
+ if (!hasStakeProtocol) {
1186
+ return null;
1187
+ }
1188
+ const solCredit = legs.find(
1189
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && leg.amount.token.symbol === "SOL" && (leg.role === "received" || leg.role === "protocol_withdraw")
1190
+ );
1191
+ if (!solCredit) {
1192
+ return null;
1193
+ }
1194
+ const solDebit = legs.find(
1195
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.amount.token.symbol === "SOL" && (leg.role === "sent" || leg.role === "protocol_deposit")
1196
+ );
1197
+ if (solDebit) {
1198
+ return null;
1199
+ }
1200
+ const withdrawer = solCredit.accountId.replace("external:", "");
1201
+ return {
1202
+ primaryType: "stake_withdraw",
1203
+ primaryAmount: solCredit.amount,
1204
+ secondaryAmount: null,
1205
+ sender: null,
1206
+ receiver: withdrawer,
1207
+ counterparty: null,
1208
+ confidence: 0.9,
1209
+ isRelevant: true,
1210
+ metadata: {
1211
+ withdraw_amount: solCredit.amount.amountUi,
1212
+ protocol: tx.protocol?.id
1213
+ }
1214
+ };
1215
+ }
1216
+ };
1217
+
1218
+ // ../classification/src/classifiers/bridge-classifier.ts
1219
+ var BridgeClassifier = class {
1220
+ name = "bridge";
1221
+ priority = 88;
1222
+ classify(context) {
1223
+ const { legs, tx } = context;
1224
+ const hasBridgeProtocol = isBridgeProtocolById(tx.protocol?.id);
1225
+ if (!hasBridgeProtocol) {
1226
+ return null;
1227
+ }
1228
+ const tokensOut = legs.filter(
1229
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "debit" && leg.role !== "fee" && (leg.role === "sent" || leg.role === "protocol_deposit")
1230
+ );
1231
+ const tokensIn = legs.filter(
1232
+ (leg) => leg.accountId.startsWith("external:") && leg.side === "credit" && (leg.role === "received" || leg.role === "protocol_withdraw")
1233
+ );
1234
+ let primaryType;
1235
+ let primaryAmount = null;
1236
+ let participant = null;
1237
+ if (tokensIn.length > 0 && tokensOut.length === 0) {
1238
+ primaryType = "bridge_in";
1239
+ const creditLeg = tokensIn[0];
1240
+ primaryAmount = creditLeg.amount;
1241
+ participant = creditLeg.accountId.replace("external:", "");
1242
+ } else if (tokensOut.length > 0 && tokensIn.length === 0) {
1243
+ primaryType = "bridge_out";
1244
+ const debitLeg = tokensOut[0];
1245
+ primaryAmount = debitLeg.amount;
1246
+ participant = debitLeg.accountId.replace("external:", "");
1247
+ } else if (tokensIn.length > 0 && tokensOut.length > 0) {
1248
+ primaryType = "bridge_in";
1249
+ const creditLeg = tokensIn[0];
1250
+ primaryAmount = creditLeg.amount;
1251
+ participant = creditLeg.accountId.replace("external:", "");
1252
+ } else {
1253
+ return null;
1254
+ }
1255
+ return {
1256
+ primaryType,
1257
+ primaryAmount,
1258
+ secondaryAmount: null,
1259
+ sender: primaryType === "bridge_out" ? participant : null,
1260
+ receiver: primaryType === "bridge_in" ? participant : null,
1261
+ counterparty: null,
1262
+ confidence: 0.9,
1263
+ isRelevant: true,
1264
+ metadata: {
1265
+ bridge_protocol: tx.protocol?.id,
1266
+ bridge_name: tx.protocol?.name
1267
+ }
1268
+ };
1269
+ }
1270
+ };
1271
+
1137
1272
  // ../classification/src/engine/classification-service.ts
1138
1273
  var ClassificationService = class {
1139
1274
  classifiers = [];
1140
1275
  constructor() {
1141
1276
  this.registerClassifier(new SolanaPayClassifier());
1277
+ this.registerClassifier(new BridgeClassifier());
1278
+ this.registerClassifier(new NftMintClassifier());
1279
+ this.registerClassifier(new StakeDepositClassifier());
1280
+ this.registerClassifier(new StakeWithdrawClassifier());
1142
1281
  this.registerClassifier(new SwapClassifier());
1143
1282
  this.registerClassifier(new AirdropClassifier());
1144
1283
  this.registerClassifier(new TransferClassifier());
@@ -1148,8 +1287,8 @@ var ClassificationService = class {
1148
1287
  this.classifiers.push(classifier);
1149
1288
  this.classifiers.sort((a, b) => b.priority - a.priority);
1150
1289
  }
1151
- classify(legs, walletAddress, tx) {
1152
- const context = { legs, walletAddress, tx };
1290
+ classify(legs, tx) {
1291
+ const context = { legs, tx };
1153
1292
  for (const classifier of this.classifiers) {
1154
1293
  const result = classifier.classify(context);
1155
1294
  if (result && result.isRelevant) {
@@ -1158,9 +1297,10 @@ var ClassificationService = class {
1158
1297
  }
1159
1298
  return {
1160
1299
  primaryType: "other",
1161
- direction: "neutral",
1162
1300
  primaryAmount: null,
1163
1301
  secondaryAmount: null,
1302
+ sender: null,
1303
+ receiver: null,
1164
1304
  counterparty: null,
1165
1305
  confidence: 0,
1166
1306
  isRelevant: false,
@@ -1169,8 +1309,8 @@ var ClassificationService = class {
1169
1309
  }
1170
1310
  };
1171
1311
  var classificationService = new ClassificationService();
1172
- function classifyTransaction(legs, walletAddress, tx) {
1173
- return classificationService.classify(legs, walletAddress, tx);
1312
+ function classifyTransaction(legs, tx) {
1313
+ return classificationService.classify(legs, tx);
1174
1314
  }
1175
1315
 
1176
1316
  // ../domain/src/tx/spam-filter.ts
@@ -1244,8 +1384,8 @@ function createIndexer(options) {
1244
1384
  );
1245
1385
  const classified = transactions.map((tx) => {
1246
1386
  tx.protocol = detectProtocol(tx.programIds);
1247
- const legs = transactionToLegs(tx, walletAddress);
1248
- const classification = classifyTransaction(legs, walletAddress, tx);
1387
+ const legs = transactionToLegs(tx);
1388
+ const classification = classifyTransaction(legs, tx);
1249
1389
  return { tx, classification, legs };
1250
1390
  });
1251
1391
  return classified;
@@ -1274,8 +1414,8 @@ function createIndexer(options) {
1274
1414
  );
1275
1415
  const classified = transactions.map((tx) => {
1276
1416
  tx.protocol = detectProtocol(tx.programIds);
1277
- const legs = transactionToLegs(tx, walletAddress);
1278
- const classification = classifyTransaction(legs, walletAddress, tx);
1417
+ const legs = transactionToLegs(tx);
1418
+ const classification = classifyTransaction(legs, tx);
1279
1419
  return { tx, classification, legs };
1280
1420
  });
1281
1421
  const nonSpam = filterSpamTransactions(classified, spamConfig);
@@ -1289,14 +1429,14 @@ function createIndexer(options) {
1289
1429
  }
1290
1430
  return accumulated.slice(0, limit);
1291
1431
  },
1292
- async getTransaction(signature2, walletAddress) {
1432
+ async getTransaction(signature2) {
1293
1433
  const tx = await fetchTransaction(client.rpc, signature2);
1294
1434
  if (!tx) {
1295
1435
  return null;
1296
1436
  }
1297
1437
  tx.protocol = detectProtocol(tx.programIds);
1298
- const legs = transactionToLegs(tx, walletAddress);
1299
- const classification = classifyTransaction(legs, walletAddress, tx);
1438
+ const legs = transactionToLegs(tx);
1439
+ const classification = classifyTransaction(legs, tx);
1300
1440
  return { tx, classification, legs };
1301
1441
  },
1302
1442
  async getRawTransaction(signature2) {