1use crate::core::events::*;
2use crate::grpc::types::EventType;
3use solana_sdk::pubkey::Pubkey;
4pub fn event_type(disc: u64, program: Option<&Pubkey>) -> Option<EventType> {
5 if disc == 18146529233607700591
6 && program.is_none_or(|p| {
7 *p == solana_sdk::pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
8 })
9 {
10 return Some(EventType::PumpFunPostCompleteBuy);
11 }
12 if disc == 3118876958563052404
13 && program.is_none_or(|p| {
14 *p == solana_sdk::pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
15 })
16 {
17 return Some(EventType::PumpFunSweepBondingCurveFee);
18 }
19 if disc == 619296439455019615
20 && program.is_none_or(|p| {
21 *p == solana_sdk::pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
22 })
23 {
24 return Some(EventType::PumpFunComplete);
25 }
26 if disc == 11927646055507993730
27 && program.is_none_or(|p| {
28 *p == solana_sdk::pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
29 })
30 {
31 return Some(EventType::PumpSwapSweepPoolFee);
32 }
33 None
34}
35pub fn parse(
36 disc: u64,
37 data: &[u8],
38 metadata: EventMetadata,
39 program: Option<&Pubkey>,
40) -> Option<DexEvent> {
41 let kind = event_type(disc, program)?;
42 if kind == EventType::PumpFunPostCompleteBuy {
43 if data.len() != 224 {
44 return None;
45 }
46 return Some(DexEvent::PumpFunPostCompleteBuy(PumpFunPostCompleteBuyEvent {
47 metadata,
48 user: Pubkey::new_from_array(data[0..32].try_into().ok()?),
49 mint: Pubkey::new_from_array(data[32..64].try_into().ok()?),
50 bonding_curve: Pubkey::new_from_array(data[64..96].try_into().ok()?),
51 quote_mint: Pubkey::new_from_array(data[96..128].try_into().ok()?),
52 timestamp: i64::from_le_bytes(data[128..136].try_into().ok()?),
53 base_out: u64::from_le_bytes(data[136..144].try_into().ok()?),
54 quote_in: u64::from_le_bytes(data[144..152].try_into().ok()?),
55 fee_basis_points: u64::from_le_bytes(data[152..160].try_into().ok()?),
56 fee: u64::from_le_bytes(data[160..168].try_into().ok()?),
57 creator_fee_basis_points: u64::from_le_bytes(data[168..176].try_into().ok()?),
58 creator_fee: u64::from_le_bytes(data[176..184].try_into().ok()?),
59 buyback_fee: u64::from_le_bytes(data[184..192].try_into().ok()?),
60 pool_base_reserves_before: u64::from_le_bytes(data[192..200].try_into().ok()?),
61 pool_quote_reserves_before: u64::from_le_bytes(data[200..208].try_into().ok()?),
62 pool_base_reserves_after: u64::from_le_bytes(data[208..216].try_into().ok()?),
63 pool_quote_reserves_after: u64::from_le_bytes(data[216..224].try_into().ok()?),
64 }));
65 }
66 if kind == EventType::PumpFunSweepBondingCurveFee {
67 if data.len() != 145 {
68 return None;
69 }
70 return Some(DexEvent::PumpFunSweepBondingCurveFee(PumpFunSweepBondingCurveFeeEvent {
71 metadata,
72 timestamp: i64::from_le_bytes(data[0..8].try_into().ok()?),
73 mint: Pubkey::new_from_array(data[8..40].try_into().ok()?),
74 bonding_curve: Pubkey::new_from_array(data[40..72].try_into().ok()?),
75 quote_mint: Pubkey::new_from_array(data[72..104].try_into().ok()?),
76 recipient: Pubkey::new_from_array(data[104..136].try_into().ok()?),
77 amount: u64::from_le_bytes(data[136..144].try_into().ok()?),
78 bucket: data[144],
79 }));
80 }
81 if kind == EventType::PumpFunComplete {
82 if data.len() != 136 && data.len() != 104 {
83 return None;
84 }
85 return Some(DexEvent::PumpFunComplete(PumpFunCompleteEvent {
86 metadata,
87 user: Pubkey::new_from_array(data[0..32].try_into().ok()?),
88 mint: Pubkey::new_from_array(data[32..64].try_into().ok()?),
89 bonding_curve: Pubkey::new_from_array(data[64..96].try_into().ok()?),
90 timestamp: i64::from_le_bytes(data[96..104].try_into().ok()?),
91 quote_mint: if data.len() == 104 {
92 solana_sdk::pubkey!("So11111111111111111111111111111111111111112")
93 } else {
94 Pubkey::new_from_array(data[104..136].try_into().ok()?)
95 },
96 }));
97 }
98 if kind == EventType::PumpSwapSweepPoolFee {
99 if data.len() != 177 {
100 return None;
101 }
102 return Some(DexEvent::PumpSwapSweepPoolFee(PumpSwapSweepPoolFeeEvent {
103 metadata,
104 timestamp: i64::from_le_bytes(data[0..8].try_into().ok()?),
105 pool: Pubkey::new_from_array(data[8..40].try_into().ok()?),
106 base_mint: Pubkey::new_from_array(data[40..72].try_into().ok()?),
107 quote_mint: Pubkey::new_from_array(data[72..104].try_into().ok()?),
108 recipient: Pubkey::new_from_array(data[104..136].try_into().ok()?),
109 payer: Pubkey::new_from_array(data[136..168].try_into().ok()?),
110 amount: u64::from_le_bytes(data[168..176].try_into().ok()?),
111 bucket: data[176],
112 }));
113 }
114 None
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct PumpMultiHopIntent {
120 pub user: Pubkey,
121 pub input_account: Pubkey,
122 pub output_account: Pubkey,
123 pub amount_in: u64,
124 pub min_amount_out: u64,
125 pub hops: Vec<PumpMultiHopAccounts>,
126}
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct PumpMultiHopAccounts {
129 pub base_mint: Pubkey,
130 pub quote_mint: Pubkey,
131 pub venue: Pubkey,
132 pub base_vault: Pubkey,
133 pub quote_vault: Pubkey,
134}
135pub fn decode_multi_hop_intent(
136 program: Pubkey,
137 data: &[u8],
138 accounts: &[Pubkey],
139) -> Option<PumpMultiHopIntent> {
140 if program != solana_sdk::pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
141 || data.len() != 24
142 || data[..8] != [43, 100, 73, 19, 233, 246, 111, 148]
143 || accounts.len() < 21
144 || (accounts.len() - 16) % 5 != 0
145 {
146 return None;
147 }
148 let amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?);
149 let min_amount_out = u64::from_le_bytes(data[16..24].try_into().ok()?);
150 if amount_in == 0 || min_amount_out == 0 {
151 return None;
152 }
153 Some(PumpMultiHopIntent {
154 user: accounts[0],
155 input_account: accounts[1],
156 output_account: accounts[2],
157 amount_in,
158 min_amount_out,
159 hops: accounts[16..]
160 .chunks_exact(5)
161 .map(|h| PumpMultiHopAccounts {
162 base_mint: h[0],
163 quote_mint: h[1],
164 venue: h[2],
165 base_vault: h[3],
166 quote_vault: h[4],
167 })
168 .collect(),
169 })
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 #[test]
176 fn official_event_layouts() {
177 let fixture: serde_json::Value =
178 serde_json::from_str(include_str!("../../tests/fixtures/pump_upgrade/events.json"))
179 .unwrap();
180 for f in fixture.as_array().unwrap() {
181 let n = f["discString"].as_str().unwrap().parse().unwrap();
182 let hex = f["body"].as_str().unwrap();
183 let body: Vec<u8> = (0..hex.len())
184 .step_by(2)
185 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
186 .collect();
187 let program = f["program"].as_str().unwrap().parse().unwrap();
188 assert!(parse(n, &body, EventMetadata::default(), Some(&program)).is_some());
189 assert!(parse(n, &body[..body.len() - 1], EventMetadata::default(), Some(&program))
190 .is_none());
191 assert!(parse(n, &body, EventMetadata::default(), Some(&Pubkey::default())).is_none());
192 }
193 assert!(parse(619296439455019615, &[0; 104], EventMetadata::default(), None).is_some());
194 }
195 #[test]
196 fn different_route_venues_do_not_merge() {
197 let mut base = DexEvent::PumpSwapSell(PumpSwapSellEvent {
198 pool: Pubkey::new_unique(),
199 user: Pubkey::new_unique(),
200 ..Default::default()
201 });
202 let inner = DexEvent::PumpSwapSell(PumpSwapSellEvent {
203 pool: Pubkey::new_unique(),
204 ..Default::default()
205 });
206 let mut unmerged = None;
207 assert!(!crate::core::merger::try_merge_events(&mut base, inner, &mut unmerged));
208 assert!(unmerged.is_some());
209 }
210 #[test]
211 fn multi_hop_curve_identity_survives_unmarked_log() {
212 for reverse in [false, true] {
213 let marked = DexEvent::PumpFunTrade(PumpFunTradeEvent {
214 mint: Pubkey::new_unique(),
215 ix_name: "multi_hop_swap".into(),
216 ..Default::default()
217 });
218 let log = DexEvent::PumpFunTrade(PumpFunTradeEvent {
219 mint: Pubkey::new_unique(),
220 ..Default::default()
221 });
222 let (mut base, inner) = if reverse { (log, marked) } else { (marked, log) };
223 let mut unmerged = None;
224 assert!(!crate::core::merger::try_merge_events(&mut base, inner, &mut unmerged));
225 assert!(unmerged.is_some());
226 }
227 }
228}