Skip to main content

sol_parser_sdk/core/events/
raw_instruction.rs

1//! Lossless outer instruction payload, without assumptions about program behavior.
2use super::EventMetadata;
3use serde::{Deserialize, Serialize};
4use solana_sdk::pubkey::Pubkey;
5
6/// Original outer instruction associated with a DEX trade callback. Account
7/// order and duplicates are preserved; unresolved ALT addresses remain None.
8#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
9pub struct RawInstructionPayload {
10    pub program_id: Pubkey,
11    pub instruction_index: usize,
12    pub accounts: Vec<Option<Pubkey>>,
13    pub account_indices: Vec<u8>,
14    pub instruction_data: Vec<u8>,
15}
16
17impl RawInstructionPayload {
18    /// Adapt a callback payload for the optional offline wrapper-rule helpers.
19    pub fn with_metadata(&self, metadata: EventMetadata) -> RawInstructionEvent {
20        RawInstructionEvent {
21            metadata,
22            dexes: raw_instruction_dexes(self.program_id, &self.accounts),
23            program_id: self.program_id,
24            instruction_index: self.instruction_index,
25            accounts: self.accounts.clone(),
26            account_indices: self.account_indices.clone(),
27            instruction_data: self.instruction_data.clone(),
28        }
29    }
30}
31
32impl From<&RawInstructionEvent> for RawInstructionPayload {
33    fn from(raw: &RawInstructionEvent) -> Self {
34        Self {
35            program_id: raw.program_id,
36            instruction_index: raw.instruction_index,
37            accounts: raw.accounts.clone(),
38            account_indices: raw.account_indices.clone(),
39            instruction_data: raw.instruction_data.clone(),
40        }
41    }
42}
43
44/// Evidence available to the parser, not a guarantee that every amount is known.
45#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
46pub enum TradeParseStatus {
47    /// Outer instruction references a DEX; no trade or direction is proven.
48    Unresolved,
49    /// Native instruction or explicitly applied wrapper rule decoded inputs.
50    #[default]
51    InstructionDecoded,
52    /// Successful execution metadata/logs establish the DEX invocation.
53    /// Executed amounts may still be unavailable when event logs are missing.
54    ExecutionConfirmed,
55}
56
57/// Compatibility alias for the original Pump.fun-specific name.
58pub type PumpFunParseStatus = TradeParseStatus;
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct RawInstructionEvent {
62    pub metadata: EventMetadata,
63    /// DEX associations from this instruction only, not the transaction-wide key table.
64    #[serde(default)]
65    pub dexes: Vec<RawInstructionDex>,
66    pub program_id: Pubkey,
67    pub instruction_index: usize,
68    /// Instruction account order, including duplicates. None means an unresolved
69    /// ALT address, never the System Program or an invented placeholder.
70    pub accounts: Vec<Option<Pubkey>>,
71    /// Indices into the transaction's static + loaded account key table.
72    pub account_indices: Vec<u8>,
73    /// Unmodified bytes, including any program-defined discriminator.
74    pub instruction_data: Vec<u8>,
75}
76
77/// Stable DEX identifiers shared by every language binding.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
79pub enum RawInstructionDexType {
80    #[serde(rename = "pumpfun")]
81    PumpFun,
82    #[serde(rename = "pumpswap")]
83    PumpSwap,
84    #[serde(rename = "raydium_launchlab")]
85    RaydiumLaunchlab,
86    #[serde(rename = "raydium_cpmm")]
87    RaydiumCpmm,
88    #[serde(rename = "raydium_clmm")]
89    RaydiumClmm,
90    #[serde(rename = "raydium_amm_v4")]
91    RaydiumAmmV4,
92    #[serde(rename = "orca_whirlpool")]
93    OrcaWhirlpool,
94    #[serde(rename = "meteora_pools")]
95    MeteoraPools,
96    #[serde(rename = "meteora_damm_v2")]
97    MeteoraDammV2,
98    #[serde(rename = "meteora_dlmm")]
99    MeteoraDlmm,
100    #[serde(rename = "meteora_dbc")]
101    MeteoraDbc,
102}
103
104impl RawInstructionDexType {
105    pub const fn as_str(self) -> &'static str {
106        match self {
107            Self::PumpFun => "pumpfun",
108            Self::PumpSwap => "pumpswap",
109            Self::RaydiumLaunchlab => "raydium_launchlab",
110            Self::RaydiumCpmm => "raydium_cpmm",
111            Self::RaydiumClmm => "raydium_clmm",
112            Self::RaydiumAmmV4 => "raydium_amm_v4",
113            Self::OrcaWhirlpool => "orca_whirlpool",
114            Self::MeteoraPools => "meteora_pools",
115            Self::MeteoraDammV2 => "meteora_damm_v2",
116            Self::MeteoraDlmm => "meteora_dlmm",
117            Self::MeteoraDbc => "meteora_dbc",
118        }
119    }
120}
121
122/// A DEX program directly targeted or referenced by this outer instruction.
123/// A reference is not proof of an executed CPI. Unknown wrapper layouts cannot
124/// establish a pool address; pool remains None rather than guessing an index.
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub struct RawInstructionDex {
127    pub dex_type: RawInstructionDexType,
128    pub program_id: Pubkey,
129    pub pool: Option<Pubkey>,
130}
131
132pub fn raw_instruction_dexes(
133    program_id: Pubkey,
134    accounts: &[Option<Pubkey>],
135) -> Vec<RawInstructionDex> {
136    use crate::grpc::program_ids::*;
137    let known = [
138        (RawInstructionDexType::PumpFun, PUMPFUN_PROGRAM),
139        (RawInstructionDexType::PumpSwap, PUMPSWAP_PROGRAM),
140        (RawInstructionDexType::RaydiumLaunchlab, RAYDIUM_LAUNCHLAB_PROGRAM),
141        (RawInstructionDexType::RaydiumCpmm, RAYDIUM_CPMM_PROGRAM),
142        (RawInstructionDexType::RaydiumClmm, RAYDIUM_CLMM_PROGRAM),
143        (RawInstructionDexType::RaydiumAmmV4, RAYDIUM_AMM_V4_PROGRAM),
144        (RawInstructionDexType::OrcaWhirlpool, ORCA_WHIRLPOOL_PROGRAM),
145        (RawInstructionDexType::MeteoraPools, METEORA_POOLS_PROGRAM),
146        (RawInstructionDexType::MeteoraDammV2, METEORA_DAMM_V2_PROGRAM),
147        (RawInstructionDexType::MeteoraDlmm, METEORA_DLMM_PROGRAM),
148        (RawInstructionDexType::MeteoraDbc, METEORA_DBC_PROGRAM),
149    ];
150    known
151        .into_iter()
152        .filter(|(_, key)| program_id == *key || accounts.contains(&Some(*key)))
153        .map(|(name, key)| RawInstructionDex { dex_type: name, program_id: key, pool: None })
154        .collect()
155}
156
157pub fn raw_instruction_matches_filter(
158    dexes: &[RawInstructionDex],
159    filter: &crate::grpc::EventTypeFilter,
160) -> bool {
161    use crate::grpc::EventType;
162    if filter.exclude_types.as_ref().is_some_and(|types| types.contains(&EventType::RawInstruction))
163    {
164        return false;
165    }
166    if filter.include_only.as_ref().is_some_and(|types| types.contains(&EventType::RawInstruction))
167    {
168        return true;
169    }
170    if dexes.is_empty() {
171        return filter.include_only.is_none();
172    }
173    dexes.iter().any(|dex| {
174        let event = match dex.dex_type {
175            RawInstructionDexType::PumpFun => EventType::PumpFunTrade,
176            RawInstructionDexType::PumpSwap => EventType::PumpSwapTrade,
177            RawInstructionDexType::RaydiumLaunchlab => EventType::RaydiumLaunchlabTrade,
178            RawInstructionDexType::RaydiumCpmm => EventType::RaydiumCpmmSwap,
179            RawInstructionDexType::RaydiumClmm => EventType::RaydiumClmmSwap,
180            RawInstructionDexType::RaydiumAmmV4 => EventType::RaydiumAmmV4Swap,
181            RawInstructionDexType::OrcaWhirlpool => EventType::OrcaWhirlpoolSwap,
182            RawInstructionDexType::MeteoraPools => EventType::MeteoraPoolsSwap,
183            RawInstructionDexType::MeteoraDammV2 => EventType::MeteoraDammV2Swap,
184            RawInstructionDexType::MeteoraDlmm => EventType::MeteoraDlmmSwap,
185            RawInstructionDexType::MeteoraDbc => EventType::MeteoraDbcSwap,
186        };
187        filter.should_include(event)
188    })
189}
190
191#[cfg(test)]
192mod enum_tests {
193    use super::*;
194    #[test]
195    fn dex_identifiers_round_trip_without_changing_json() {
196        for (kind, value) in [
197            (RawInstructionDexType::PumpFun, "pumpfun"),
198            (RawInstructionDexType::PumpSwap, "pumpswap"),
199            (RawInstructionDexType::RaydiumLaunchlab, "raydium_launchlab"),
200            (RawInstructionDexType::RaydiumCpmm, "raydium_cpmm"),
201            (RawInstructionDexType::RaydiumClmm, "raydium_clmm"),
202            (RawInstructionDexType::RaydiumAmmV4, "raydium_amm_v4"),
203            (RawInstructionDexType::OrcaWhirlpool, "orca_whirlpool"),
204            (RawInstructionDexType::MeteoraPools, "meteora_pools"),
205            (RawInstructionDexType::MeteoraDammV2, "meteora_damm_v2"),
206            (RawInstructionDexType::MeteoraDlmm, "meteora_dlmm"),
207            (RawInstructionDexType::MeteoraDbc, "meteora_dbc"),
208        ] {
209            assert_eq!(serde_json::to_value(kind).unwrap(), value);
210            assert_eq!(
211                serde_json::from_value::<RawInstructionDexType>(serde_json::json!(value)).unwrap(),
212                kind
213            );
214        }
215        assert!(serde_json::from_str::<RawInstructionDexType>("\"typo\"").is_err());
216    }
217}