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