Skip to main content

sol_parser_sdk/logs/
pump_upgrade.rs

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/// Instruction intent only. Each hop's executed amounts come from its trade events.
118#[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}