1use crate::{core::events::*, instr::read_bool};
2use solana_sdk::pubkey::Pubkey;
3use std::collections::HashMap;
4use yellowstone_grpc_proto::prelude::{Transaction, TransactionStatusMeta};
5
6#[inline]
7fn set_pumpswap_is_pump_pool_from_fees_ix(
8 meta: &TransactionStatusMeta,
9 transaction: &Option<Transaction>,
10 program_invokes: &HashMap<Pubkey, Vec<(i32, i32)>>,
11 is_pump_pool: &mut bool,
12) {
13 if let Some(invoke) =
14 program_invokes.get(&crate::grpc::program_ids::PUMPSWAP_FEES_PROGRAM).and_then(|v| v.last())
15 {
16 if let Some(data) = get_instruction_data(meta, transaction, invoke) {
17 *is_pump_pool = read_bool(data, 9).unwrap_or_default();
18 }
19 }
20}
21
22#[inline]
23pub fn fill_data(
24 event: &mut DexEvent,
25 meta: &TransactionStatusMeta,
26 transaction: &Option<Transaction>,
27 program_invokes: &HashMap<Pubkey, Vec<(i32, i32)>>,
28) {
29 match event {
30 DexEvent::PumpSwapBuy(ref mut e) => {
31 set_pumpswap_is_pump_pool_from_fees_ix(
32 meta,
33 transaction,
34 program_invokes,
35 &mut e.is_pump_pool,
36 );
37 }
38 DexEvent::PumpSwapSell(ref mut e) => {
39 set_pumpswap_is_pump_pool_from_fees_ix(
40 meta,
41 transaction,
42 program_invokes,
43 &mut e.is_pump_pool,
44 );
45 }
46 _ => {}
47 }
48}
49
50#[inline]
56pub fn fill_token_balances(
57 event: &mut DexEvent,
58 meta: &TransactionStatusMeta,
59 transaction: &Option<Transaction>,
60) {
61 let trade = match event {
62 DexEvent::PumpFunTrade(e)
63 | DexEvent::PumpFunBuy(e)
64 | DexEvent::PumpFunSell(e)
65 | DexEvent::PumpFunBuyExactSolIn(e) => e,
66 _ => return,
67 };
68
69 if let Some(user_index) = account_index(transaction, meta, &trade.user) {
70 trade.pre_sol_balance = meta.pre_balances.get(user_index).copied();
71 trade.post_sol_balance = meta.post_balances.get(user_index).copied();
72 }
73
74 let Some(token_index) = token_account_index(transaction, meta, &trade.associated_user) else {
75 return;
76 };
77 let token_index = token_index as u32;
78 let pre = meta
79 .pre_token_balances
80 .iter()
81 .find(|balance| balance.account_index == token_index)
82 .map(crate::grpc::transaction_meta::token_balance_raw_amount);
83 let post = meta
84 .post_token_balances
85 .iter()
86 .find(|balance| balance.account_index == token_index)
87 .map(crate::grpc::transaction_meta::token_balance_raw_amount);
88
89 if pre.is_some() || post.is_some() {
90 trade.pre_token_balance = Some(pre.unwrap_or(0));
91 trade.post_token_balance = Some(post.unwrap_or(0));
92 }
93}
94
95#[inline]
96fn account_index(
97 transaction: &Option<Transaction>,
98 meta: &TransactionStatusMeta,
99 account: &Pubkey,
100) -> Option<usize> {
101 if *account == Pubkey::default() {
102 return None;
103 }
104
105 let message = transaction.as_ref()?.message.as_ref()?;
106 message
107 .account_keys
108 .iter()
109 .chain(meta.loaded_writable_addresses.iter())
110 .chain(meta.loaded_readonly_addresses.iter())
111 .position(|key| key.as_slice() == account.as_ref())
112}
113
114#[inline]
115fn token_account_index(
116 transaction: &Option<Transaction>,
117 meta: &TransactionStatusMeta,
118 account: &Pubkey,
119) -> Option<usize> {
120 if *account == Pubkey::default() {
121 return None;
122 }
123
124 let message = transaction.as_ref()?.message.as_ref()?;
125 let static_len = message.account_keys.len();
126 let writable_len = meta.loaded_writable_addresses.len();
127
128 meta.pre_token_balances.iter().chain(meta.post_token_balances.iter()).find_map(|balance| {
129 let index = balance.account_index as usize;
130 let key = if index < static_len {
131 message.account_keys.get(index)
132 } else if index < static_len + writable_len {
133 meta.loaded_writable_addresses.get(index - static_len)
134 } else {
135 meta.loaded_readonly_addresses.get(index - static_len - writable_len)
136 }?;
137
138 (key.as_slice() == account.as_ref()).then_some(index)
139 })
140}
141
142pub fn get_instruction_data<'a>(
143 meta: &'a TransactionStatusMeta,
144 transaction: &'a Option<Transaction>,
145 index: &(i32, i32), ) -> Option<&'a [u8]> {
147 let data = if index.1 >= 0 {
148 meta.inner_instructions
149 .iter()
150 .find(|i| i.index == index.0 as u32)?
151 .instructions
152 .get(index.1 as usize)?
153 .data
154 .as_slice()
155 } else {
156 transaction.as_ref()?.message.as_ref()?.instructions.get(index.0 as usize)?.data.as_slice()
157 };
158 Some(data)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use yellowstone_grpc_proto::prelude::{Message, TokenBalance, UiTokenAmount};
165
166 fn token_balance(account_index: u32, amount: u64) -> TokenBalance {
167 TokenBalance {
168 account_index,
169 ui_token_amount: Some(UiTokenAmount {
170 amount: amount.to_string(),
171 ..Default::default()
172 }),
173 ..Default::default()
174 }
175 }
176
177 fn pumpfun_event(user: Pubkey, associated_user: Pubkey) -> DexEvent {
178 DexEvent::PumpFunBuy(PumpFunTradeEvent {
179 user,
180 associated_user,
181 is_buy: true,
182 ..Default::default()
183 })
184 }
185
186 fn transaction(accounts: &[Pubkey]) -> Option<Transaction> {
187 Some(Transaction {
188 message: Some(Message {
189 account_keys: accounts.iter().map(|key| key.to_bytes().to_vec()).collect(),
190 ..Default::default()
191 }),
192 ..Default::default()
193 })
194 }
195
196 #[test]
197 fn fills_pumpfun_user_token_and_sol_balances() {
198 let user = Pubkey::new_unique();
199 let associated_user = Pubkey::new_unique();
200 let mut event = pumpfun_event(user, associated_user);
201 let meta = TransactionStatusMeta {
202 pre_balances: vec![50_000, 1_000],
203 post_balances: vec![40_000, 1_000],
204 pre_token_balances: vec![token_balance(1, 10)],
205 post_token_balances: vec![token_balance(1, 35)],
206 ..Default::default()
207 };
208
209 fill_token_balances(&mut event, &meta, &transaction(&[user, associated_user]));
210
211 let DexEvent::PumpFunBuy(trade) = event else { panic!("expected PumpFun buy") };
212 assert_eq!(trade.pre_sol_balance, Some(50_000));
213 assert_eq!(trade.post_sol_balance, Some(40_000));
214 assert_eq!(trade.pre_token_balance, Some(10));
215 assert_eq!(trade.post_token_balance, Some(35));
216 }
217
218 #[test]
219 fn missing_token_balance_side_means_zero_for_created_or_closed_account() {
220 let user = Pubkey::new_unique();
221 let associated_user = Pubkey::new_unique();
222 let tx = transaction(&[user, associated_user]);
223
224 let mut created = pumpfun_event(user, associated_user);
225 let created_meta = TransactionStatusMeta {
226 pre_balances: vec![50, 0],
227 post_balances: vec![40, 1],
228 post_token_balances: vec![token_balance(1, 25)],
229 ..Default::default()
230 };
231 fill_token_balances(&mut created, &created_meta, &tx);
232 let DexEvent::PumpFunBuy(created) = created else { unreachable!() };
233 assert_eq!(created.pre_token_balance, Some(0));
234 assert_eq!(created.post_token_balance, Some(25));
235
236 let mut closed = pumpfun_event(user, associated_user);
237 let closed_meta = TransactionStatusMeta {
238 pre_balances: vec![40, 1],
239 post_balances: vec![45, 0],
240 pre_token_balances: vec![token_balance(1, 25)],
241 ..Default::default()
242 };
243 fill_token_balances(&mut closed, &closed_meta, &tx);
244 let DexEvent::PumpFunBuy(closed) = closed else { unreachable!() };
245 assert_eq!(closed.pre_token_balance, Some(25));
246 assert_eq!(closed.post_token_balance, Some(0));
247 }
248
249 #[test]
250 fn ignores_non_pumpfun_events() {
251 let mut event = DexEvent::Error("unchanged".to_string());
252
253 fill_token_balances(&mut event, &TransactionStatusMeta::default(), &None);
254
255 assert!(matches!(event, DexEvent::Error(ref message) if message == "unchanged"));
256 }
257
258 #[test]
259 fn resolves_user_token_account_from_loaded_writable_addresses() {
260 let user = Pubkey::new_unique();
261 let associated_user = Pubkey::new_unique();
262 let mut event = pumpfun_event(user, associated_user);
263 let tx = transaction(&[user]);
264 let meta = TransactionStatusMeta {
265 pre_balances: vec![50, 1],
266 post_balances: vec![40, 1],
267 loaded_writable_addresses: vec![associated_user.to_bytes().to_vec()],
268 pre_token_balances: vec![token_balance(1, 10)],
269 post_token_balances: vec![token_balance(1, 35)],
270 ..Default::default()
271 };
272
273 fill_token_balances(&mut event, &meta, &tx);
274
275 let DexEvent::PumpFunBuy(trade) = event else { unreachable!() };
276 assert_eq!(trade.pre_token_balance, Some(10));
277 assert_eq!(trade.post_token_balance, Some(35));
278 }
279
280 #[test]
281 fn ignores_out_of_range_token_balance_account_index() {
282 let user = Pubkey::new_unique();
283 let associated_user = Pubkey::new_unique();
284 let mut event = pumpfun_event(user, associated_user);
285 let meta = TransactionStatusMeta {
286 pre_balances: vec![50, 1],
287 post_balances: vec![40, 1],
288 pre_token_balances: vec![token_balance(99, 10)],
289 post_token_balances: vec![token_balance(99, 35)],
290 ..Default::default()
291 };
292
293 fill_token_balances(&mut event, &meta, &transaction(&[user, associated_user]));
294
295 let DexEvent::PumpFunBuy(trade) = event else { unreachable!() };
296 assert_eq!(trade.pre_token_balance, None);
297 assert_eq!(trade.post_token_balance, None);
298 }
299}