Skip to main content

sol_parser_sdk/
stonkfun_registry.rs

1//! Caller-owned provenance index for successfully migrated StonkFun CPMM pools.
2//! Persist it with serde; it is not a network-wide pool discovery service.
3use crate::{
4    core::events::{RaydiumLaunchlabMigrateAmmEvent, StonkFunMode},
5    DexEvent,
6};
7use serde::{Deserialize, Serialize};
8use solana_sdk::{pubkey::Pubkey, signature::Signature};
9use solana_transaction_status::EncodedConfirmedTransactionWithStatusMeta;
10use std::collections::BTreeMap;
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct StonkFunGraduatedPool {
14    pub curve_pool: Pubkey,
15    pub pool: Pubkey,
16    pub base_mint: Pubkey,
17    pub quote_mint: Pubkey,
18    pub platform_config: Pubkey,
19    pub migration_signature: Signature,
20    pub migration_slot: u64,
21}
22
23#[cfg(test)]
24mod tests {
25    use super::*;
26    fn migration() -> DexEvent {
27        let mut accounts: Vec<_> = (0..28).map(|_| Pubkey::new_unique()).collect();
28        accounts[3] = crate::core::events::STONKFUN_REWARD_PLATFORM_CONFIG;
29        accounts[4] = crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID;
30        crate::instr::raydium_launchlab::parse_instruction(
31            &crate::instr::raydium_launchlab::discriminators::MIGRATE_TO_CPSWAP,
32            &accounts,
33            Signature::new_unique(),
34            123,
35            0,
36            None,
37        )
38        .unwrap()
39    }
40    #[test]
41    fn successful_migrations_are_replay_safe_and_json_persistent() {
42        let event = migration();
43        let mut registry = StonkFunPoolRegistry::default();
44        assert_eq!(registry.observe_events(&[event.clone()], false).unwrap(), 0);
45        assert!(registry.verified_cpmm_pools().is_empty());
46        assert_eq!(registry.observe_events(&[event.clone(), event.clone()], true).unwrap(), 1);
47        assert_eq!(registry.observe_events(&[event.clone()], true).unwrap(), 0);
48        let DexEvent::RaydiumLaunchlabMigrateAmm(e) = event else { panic!("migration") };
49        assert_eq!(registry.pools_for_base_mint(&e.base_mint)[0].quote_mint, e.quote_mint);
50        let json = serde_json::to_string(&registry).unwrap();
51        let restored: StonkFunPoolRegistry = serde_json::from_str(&json).unwrap();
52        assert_eq!(restored.get(&e.new_pool), registry.get(&e.new_pool));
53    }
54    #[test]
55    fn conflicting_batch_is_atomic_and_foreign_platforms_are_ignored() {
56        let original = migration();
57        let mut registry = StonkFunPoolRegistry::default();
58        registry.observe_events(&[original.clone()], true).unwrap();
59        let mut conflicting = original.clone();
60        let DexEvent::RaydiumLaunchlabMigrateAmm(e) = &mut conflicting else { panic!("migration") };
61        e.quote_mint = Pubkey::new_unique();
62        let before = serde_json::to_string(&registry).unwrap();
63        assert!(registry.observe_events(&[migration(), conflicting], true).is_err());
64        assert_eq!(serde_json::to_string(&registry).unwrap(), before);
65        let mut foreign = migration();
66        let DexEvent::RaydiumLaunchlabMigrateAmm(e) = &mut foreign else { panic!("migration") };
67        e.platform_config = Pubkey::new_unique();
68        assert_eq!(registry.observe_events(&[foreign], true).unwrap(), 0);
69    }
70}
71
72impl StonkFunGraduatedPool {
73    pub fn mode(&self) -> Option<StonkFunMode> {
74        crate::core::events::stonkfun_mode_from_platform_config(self.platform_config)
75    }
76}
77
78/// Entries are sorted by pool for deterministic persistence. Ingest only data
79/// from a trusted RPC/subscription, with finalized status if rollback matters.
80#[derive(Clone, Debug, Default, Serialize, Deserialize)]
81pub struct StonkFunPoolRegistry {
82    #[serde(with = "pool_entries")]
83    pools: BTreeMap<Pubkey, StonkFunGraduatedPool>,
84}
85
86mod pool_entries {
87    use super::*;
88    pub fn serialize<S: serde::Serializer>(
89        pools: &BTreeMap<Pubkey, StonkFunGraduatedPool>,
90        serializer: S,
91    ) -> Result<S::Ok, S::Error> {
92        pools.values().collect::<Vec<_>>().serialize(serializer)
93    }
94    pub fn deserialize<'de, D: serde::Deserializer<'de>>(
95        deserializer: D,
96    ) -> Result<BTreeMap<Pubkey, StonkFunGraduatedPool>, D::Error> {
97        let entries = Vec::<StonkFunGraduatedPool>::deserialize(deserializer)?;
98        let mut pools = BTreeMap::new();
99        for entry in entries {
100            if entry.mode().is_none()
101                || [entry.curve_pool, entry.pool, entry.base_mint, entry.quote_mint]
102                    .contains(&Pubkey::default())
103                || entry.curve_pool == entry.pool
104                || entry.base_mint == entry.quote_mint
105                || pools.contains_key(&entry.pool)
106            {
107                return Err(serde::de::Error::custom(
108                    "Invalid or duplicate StonkFun registry entry",
109                ));
110            }
111            pools.insert(entry.pool, entry);
112        }
113        Ok(pools)
114    }
115}
116
117impl StonkFunPoolRegistry {
118    pub fn get(&self, pool: &Pubkey) -> Option<&StonkFunGraduatedPool> {
119        self.pools.get(pool)
120    }
121
122    pub fn pools_for_base_mint(&self, mint: &Pubkey) -> Vec<&StonkFunGraduatedPool> {
123        self.pools.values().filter(|entry| entry.base_mint == *mint).collect()
124    }
125
126    /// Supply these identities to the route analyzer for graduated attribution.
127    pub fn verified_cpmm_pools(&self) -> Vec<Pubkey> {
128        self.pools.keys().copied().collect()
129    }
130
131    /// Replay-safe and atomic: conflicting provenance leaves the index unchanged.
132    /// The execution flag must come from transaction metadata, not log presence.
133    pub fn observe_events(
134        &mut self,
135        events: &[DexEvent],
136        succeeded: bool,
137    ) -> Result<usize, String> {
138        if !succeeded {
139            return Ok(0);
140        }
141        let mut pending = BTreeMap::new();
142        for event in events {
143            let DexEvent::RaydiumLaunchlabMigrateAmm(event) = event else { continue };
144            let Some(entry) = Self::from_migration(event) else { continue };
145            if let Some(previous) = pending.get(&entry.pool).or_else(|| self.pools.get(&entry.pool))
146            {
147                // RPC and stream metadata can assign different receive/index data.
148                // Identity fields and original migration provenance must agree.
149                if previous != &entry {
150                    return Err(format!(
151                        "Conflicting StonkFun migration provenance for {}",
152                        entry.pool
153                    ));
154                }
155            } else {
156                pending.insert(entry.pool, entry);
157            }
158        }
159        let count = pending.len();
160        self.pools.extend(pending);
161        Ok(count)
162    }
163
164    pub fn observe_rpc_transaction(
165        &mut self,
166        transaction: &EncodedConfirmedTransactionWithStatusMeta,
167    ) -> Result<usize, String> {
168        let succeeded = transaction
169            .transaction
170            .meta
171            .as_ref()
172            .ok_or("Transaction metadata is missing")?
173            .err
174            .is_none();
175        if !succeeded {
176            return Ok(0);
177        }
178        let events =
179            crate::parse_rpc_transaction(transaction, None).map_err(|error| error.to_string())?;
180        self.observe_events(&events, true)
181    }
182
183    fn from_migration(event: &RaydiumLaunchlabMigrateAmmEvent) -> Option<StonkFunGraduatedPool> {
184        if event.stonkfun_mode().is_none()
185            || event.destination_program != crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID
186            || [event.old_pool, event.new_pool, event.base_mint, event.quote_mint]
187                .contains(&Pubkey::default())
188            || event.old_pool == event.new_pool
189            || event.base_mint == event.quote_mint
190        {
191            return None;
192        }
193        Some(StonkFunGraduatedPool {
194            curve_pool: event.old_pool,
195            pool: event.new_pool,
196            base_mint: event.base_mint,
197            quote_mint: event.quote_mint,
198            platform_config: event.platform_config,
199            migration_signature: event.metadata.signature,
200            migration_slot: event.metadata.slot,
201        })
202    }
203}