sol_parser_sdk/
stonkfun_registry.rs1use 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(®istry).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(®istry).unwrap();
63 assert!(registry.observe_events(&[migration(), conflicting], true).is_err());
64 assert_eq!(serde_json::to_string(®istry).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#[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 pub fn verified_cpmm_pools(&self) -> Vec<Pubkey> {
128 self.pools.keys().copied().collect()
129 }
130
131 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 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}