1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
use base64::Engine;
use core::panic;
use log::info;
use std::str::FromStr;

use solana_sdk::{pubkey::Pubkey, transaction::Transaction};
use solana_transaction_status::{
    option_serializer::OptionSerializer,
    EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
    UiInstruction, UiMessage, UiParsedInstruction,
};
use timed::timed;

use crate::{constants, util, Swap};

#[derive(Debug, Default)]
pub struct NewPool {
    pub amm_pool_id: Pubkey,
    pub input_mint: Pubkey,
    pub output_mint: Pubkey,
    pub creator: Pubkey,
}

#[timed(duration(printer = "info!"))]
pub fn parse_mint(
    tx: &EncodedConfirmedTransactionWithStatusMeta,
) -> Result<String, Box<dyn std::error::Error>> {
    let instructions = self::parse_instructions(tx)?;
    for instruction in instructions {
        if let UiInstruction::Parsed(ix) = instruction {
            match ix {
                UiParsedInstruction::Parsed(ix) => {
                    if ix.program == "spl-associated-token-account" {
                        // TODO this might panic, might be handled more gracefully
                        let mint = ix.parsed["info"]["mint"]
                            .as_str()
                            .unwrap()
                            .to_string();
                        return Ok(mint);
                    }
                }
                UiParsedInstruction::PartiallyDecoded(_) => (),
            }
        }
    }
    Err("Mint not found in tx".into())
}

#[timed(duration(printer = "info!"))]
pub fn parse_tmp_account(
    tx: &EncodedConfirmedTransactionWithStatusMeta,
) -> Result<String, Box<dyn std::error::Error>> {
    let instructions = self::parse_instructions(tx)?;
    let mut tmp_account = String::new();
    for instruction in instructions {
        if let UiInstruction::Parsed(ix) = instruction {
            match ix {
                UiParsedInstruction::Parsed(ix) => {
                    if ix.program == "spl-token"
                        && ix.parsed["type"] == "closeAccount"
                    {
                        tmp_account = ix.parsed["info"]["account"].to_string();
                    }
                }
                UiParsedInstruction::PartiallyDecoded(_) => {}
            }
        }
    }

    if tmp_account.is_empty() {
        return Err("Temp account not found".into());
    }

    Ok(tmp_account)
}

pub fn parse_signer() -> Result<String, Box<dyn std::error::Error>> {
    // TODO
    Err("Not implemented".into())
}

#[timed(duration(printer = "info!"))]
pub fn parse_notional(
    tx: &EncodedConfirmedTransactionWithStatusMeta,
) -> Result<u64, Box<dyn std::error::Error>> {
    if let Some(meta) = &tx.transaction.meta {
        let max_sol = std::iter::zip(&meta.pre_balances, &meta.post_balances)
            .map(|(a, b)| (*a as f64 - *b as f64) as u64)
            .max()
            .unwrap();
        return Ok(max_sol);
    }
    Err("could not parse notional".into())
}

pub fn deserialize<T: Clone>(item: &OptionSerializer<T>) -> T {
    match item {
        OptionSerializer::Some(val) => val.clone(),
        _ => panic!("Deserialization failed"),
    }
}

#[timed(duration(printer = "info!"))]
pub fn parse_new_pool(
    tx: &EncodedConfirmedTransactionWithStatusMeta,
) -> Result<NewPool, Box<dyn std::error::Error>> {
    let mut pool_info = NewPool::default();
    if let Some(meta) = &tx.transaction.meta {
        for ixs in self::deserialize(&meta.inner_instructions) {
            for ix in ixs.instructions.iter().rev() {
                if let UiInstruction::Parsed(UiParsedInstruction::Parsed(
                    parsed_ix,
                )) = ix
                {
                    if parsed_ix.parsed["type"] == "assign"
                        && parsed_ix.parsed["info"]["owner"]
                            == constants::RAYDIUM_LIQUIDITY_POOL_V4_PUBKEY
                        && pool_info.amm_pool_id == Pubkey::default()
                    {
                        pool_info.amm_pool_id = Pubkey::from_str(
                            parsed_ix.parsed["info"]["account"]
                                .as_str()
                                .unwrap(),
                        )
                        .unwrap();
                    }

                    if parsed_ix.parsed["type"] == "initializeAccount"
                        && parsed_ix.parsed["info"]["owner"]
                            == constants::RAYDIUM_AUTHORITY_V4_PUBKEY
                    {
                        let mint =
                            parsed_ix.parsed["info"]["mint"].as_str().unwrap();
                        if mint == constants::SOLANA_PROGRAM_ID {
                            pool_info.input_mint =
                                Pubkey::from_str(constants::SOLANA_PROGRAM_ID)
                                    .unwrap();
                        } else {
                            pool_info.output_mint =
                                Pubkey::from_str(mint).unwrap();
                        }
                    }

                    if parsed_ix.program == "system" {
                        if let Some(owner) =
                            parsed_ix.parsed["info"]["source"].as_str()
                        {
                            pool_info.creator =
                                Pubkey::from_str(owner).unwrap();
                        }
                    }
                }
            }
        }
    }
    Ok(pool_info)
}

#[timed(duration(printer = "info!"))]
pub fn parse_swap(
    tx: &EncodedConfirmedTransactionWithStatusMeta,
) -> Result<Swap, Box<dyn std::error::Error>> {
    let mut swap = Swap::default();
    if let Some(meta) = &tx.transaction.meta {
        let all_ixs = self::deserialize(&meta.inner_instructions);
        for ixs in all_ixs {
            // might also be identified based on static index 5 but
            // that would be even more brittle than this
            if ixs.instructions.len() == 2 {
                for ix in ixs.instructions {
                    if let UiInstruction::Parsed(
                        UiParsedInstruction::Parsed(parsed_ix),
                    ) = ix
                    {
                        if parsed_ix.program == "spl-token"
                            && parsed_ix.parsed["type"] == "transfer"
                        {
                            let amount = parsed_ix.parsed["info"]["amount"]
                                .as_str()
                                .unwrap()
                                .parse::<f64>()
                                .unwrap();
                            // if the authority is raydium, it is the shitcoin, otherwise SOL
                            if parsed_ix.parsed["info"]["authority"]
                                == constants::RAYDIUM_AUTHORITY_V4_PUBKEY
                            {
                                // shitcoin == base quote, like POOP/SOL
                                swap.base_mint = self::parse_mint(tx)?;
                                swap.base_amount = amount;
                            } else {
                                // TODO not sure how to support non-SOL
                                // swaps yet also does not return the
                                // mint token properly
                                swap.quote_mint =
                                    constants::SOLANA_PROGRAM_ID.to_string();
                                swap.quote_amount = amount;
                            };
                        }
                    }
                }
                swap.sol_amount_ui =
                    util::lamports_to_sol(swap.quote_amount as u64);
            }
        }
    }

    Ok(swap)
}

#[timed(duration(printer = "info!"))]
pub fn parse_instructions(
    tx: &EncodedConfirmedTransactionWithStatusMeta,
) -> Result<Vec<UiInstruction>, Box<dyn std::error::Error>> {
    match &tx.transaction.transaction {
        EncodedTransaction::Json(ui_tx) => match &ui_tx.message {
            UiMessage::Parsed(msg) => Ok(msg.instructions.clone()),
            UiMessage::Raw(_) => Err("Raw message not supported".into()),
        },
        _ => Err("Only EncodedTransaction::Json txs are supported".into()),
    }
}

/// decode_tx decodes a base64 transaction (send from another txlisten service in Go)
/// this won't be used since the transaction format differs to what is expected
/// (EncodedTransactionWithStatusMeta in Rust)
/// solana SDK is trashy I must say..
pub fn decode_tx(raw_tx: String) -> Result<(), Box<dyn std::error::Error>> {
    let raw_bytes = base64::prelude::BASE64_STANDARD.decode(raw_tx)?;
    let tx: Transaction = bincode::deserialize(&raw_bytes)?;

    println!("{:?}", tx);
    Ok(())
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_decode_tx() {
        let raw_tx = "AUqzoqR28ec7nh+XuPEaQ8GDEJRtpdlg+kILyL1G2L7kQ836DqwhnP3AvpqaiR6TmcQAGXwITv5vf0kXu3gzkgeAAQAMF847x3GeLV4VO7eN63MSkgp+jLbu8dka4OKEBgV8ak44ib5hiJjILmxsUCBjFgn2Y4f7QVjWdziegObSFvuzPznNlQVsnTFEmlxsOoCriIOCXTyj25c2tlOF7ZzEKjvB26bZiJ4Hp1o/26BYg9csa7uNdsxPX+gRuGhVECd45oLs/YVwGKWUV2zfHZE2qkR1ddcRpoZU2qv56tYLo+pLf/dTU0/nk/TVeZYR3wgCK+eFqp79zzNVkIzse9MiwmTYW79BkWX5JaN29q8gjSENqQ6KzYMz1INh3Dn4T0F9MzYwI4WjpKu1O+YYZBbhsgVU7zh/COag0oI/cp40afjQFBlhUcscPp0g/YRKOoHODeiXX/AHv7/KUbB30tLvBJnYDkCJDBbp4Rc5wIo5MjGjRzeuQLlliyB4Fqg1PomFbG3fBAU+rsk3XIxisOEl64y2KaBE51AzJolm/VGwofTutTUDBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkGm4hX/quBhPtof2NGGMA12sQ53BrrO1WYoPAAAAAAAQan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAS9lJxDYCwz8gd5DtFqNSTKG5l1zxIaKpDP/sffi2is2MlyWPTiSJ8bs9ECkUjg2DC1oTmdr/EIQEjnvY2+n4WUFXsFgPMcX85EpiWC28+deO51lDoISjk7NQNo0iiZMI0hWQJd+mLVEHAODIJNao5niz8Efr3wgqrc/u/TZz5u15/f3z/y6DYV6qShd68DAYic45gTgcm5TTuPJ8CAeYLA0HUagoLaYTBf4pnDe5mOWEcdsRNQNzEPi+EEWmCvbu04oIcnkuTE3nX4rVkwcCBnT6Djny2yHDBDANkf2+Bz70YiUZmfT1BiEModHgTed58H5UTgPBni7NojS7m0KecAYLAAkDQUIPAAAAAAALAAUCQEIPAAwCAAF8AwAAAM47x3GeLV4VO7eN63MSkgp+jLbu8dka4OKEBgV8ak44IAAAAAAAAABGUFJScFpLOWs3Z21hOGlVc3BEd2JTaVlTUkZvVm45SvCRWqQLAAAApQAAAAAAAAAG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQ0EAQ4ADwEBEBUNEQwPAhIDBBMOBQYHFAgVFgAJAQoaAf6421FmAAAAAAB0O6QLAAAAAADaSTtxfQwNAwEAAAEJAA==".to_string();
        super::decode_tx(raw_tx).unwrap();
    }
}