1use fusionamm_client::{
12 fetch_all_position_with_filter, get_bundled_position_address, get_position_address, get_position_bundle_address, DecodedAccount, Position,
13 PositionBundle, PositionFilter,
14};
15use fusionamm_core::POSITION_BUNDLE_SIZE;
16use solana_account::Account;
17use solana_client::{nonblocking::rpc_client::RpcClient, rpc_request::TokenAccountsFilter};
18use solana_pubkey::Pubkey;
19use std::{collections::HashMap, error::Error};
20
21use crate::{get_token_accounts_for_owner, ParsedTokenAccount};
22
23#[derive(Debug)]
29pub struct HydratedPosition {
30 pub address: Pubkey,
32
33 pub data: Position,
35
36 pub token_program: Pubkey,
38}
39
40#[derive(Debug)]
45pub struct HydratedBundledPosition {
46 pub address: Pubkey,
48
49 pub data: Position,
51}
52
53#[derive(Debug)]
58pub struct HydratedPositionBundle {
59 pub address: Pubkey,
61
62 pub data: PositionBundle,
64
65 pub positions: Vec<HydratedBundledPosition>,
67
68 pub token_program: Pubkey,
70}
71
72#[derive(Debug)]
77pub enum PositionOrBundle {
78 Position(HydratedPosition),
80
81 PositionBundle(HydratedPositionBundle),
83}
84
85fn get_position_in_bundle_addresses(position_bundle: &PositionBundle) -> Vec<Pubkey> {
86 let mut positions: Vec<Pubkey> = Vec::new();
87 for i in 0..POSITION_BUNDLE_SIZE {
88 let byte_index = i / 8;
89 let bit_index = i % 8;
90 if position_bundle.position_bitmap[byte_index] & (1 << bit_index) != 0 {
91 let result = get_bundled_position_address(&position_bundle.position_bundle_mint, i as u8);
92 if let Ok(result) = result {
93 positions.push(result.0);
94 }
95 }
96 }
97 positions
98}
99
100pub async fn fetch_positions_for_owner(rpc: &RpcClient, owner: Pubkey) -> Result<Vec<PositionOrBundle>, Box<dyn Error>> {
145 let token_accounts = get_token_accounts_for_owner(rpc, owner, TokenAccountsFilter::ProgramId(spl_token::ID)).await?;
146 let token_extension_accounts = get_token_accounts_for_owner(rpc, owner, TokenAccountsFilter::ProgramId(spl_token_2022::ID)).await?;
147
148 let potiential_tokens: Vec<ParsedTokenAccount> = [token_accounts, token_extension_accounts]
149 .into_iter()
150 .flatten()
151 .filter(|x| x.amount == 1)
152 .collect();
153
154 let position_addresses: Vec<Pubkey> = potiential_tokens
155 .iter()
156 .map(|x| get_position_address(&x.mint).map(|x| x.0))
157 .collect::<Result<Vec<Pubkey>, _>>()?;
158
159 let position_bundle_addresses: Vec<Pubkey> = potiential_tokens
160 .iter()
161 .map(|x| get_position_bundle_address(&x.mint).map(|x| x.0))
162 .collect::<Result<Vec<Pubkey>, _>>()?;
163
164 let position_infos = rpc.get_multiple_accounts(&position_addresses).await?;
165
166 let positions: Vec<Option<Position>> = position_infos
167 .iter()
168 .map(|x| x.as_ref().and_then(|x| Position::from_bytes(&x.data).ok()))
169 .collect();
170
171 let position_bundle_infos = rpc.get_multiple_accounts(&position_bundle_addresses).await?;
172
173 let position_bundles: Vec<Option<PositionBundle>> = position_bundle_infos
174 .iter()
175 .map(|x| x.as_ref().and_then(|x| PositionBundle::from_bytes(&x.data).ok()))
176 .collect();
177
178 let bundled_positions_addresses: Vec<Pubkey> = position_bundles.iter().flatten().flat_map(get_position_in_bundle_addresses).collect();
179
180 let bundled_positions_infos: Vec<Account> = rpc
181 .get_multiple_accounts(&bundled_positions_addresses)
182 .await?
183 .into_iter()
184 .flatten()
185 .collect();
186
187 let mut bundled_positions_map: HashMap<Pubkey, Vec<(Pubkey, Position)>> = HashMap::new();
188 for i in 0..bundled_positions_addresses.len() {
189 let bundled_position_address = bundled_positions_addresses[i];
190 let bundled_position_info = &bundled_positions_infos[i];
191 let position = Position::from_bytes(&bundled_position_info.data)?;
192 let key = position.position_mint;
193 bundled_positions_map.entry(key).or_default();
194 if let Some(x) = bundled_positions_map.get_mut(&key) {
195 x.push((bundled_position_address, position))
196 }
197 }
198
199 let mut position_or_bundles: Vec<PositionOrBundle> = Vec::new();
200
201 for i in 0..potiential_tokens.len() {
202 let position = &positions[i];
203 let position_bundle = &position_bundles[i];
204 let token_account = &potiential_tokens[i];
205
206 if let Some(position) = position {
207 let position_address = position_addresses[i];
208 position_or_bundles.push(PositionOrBundle::Position(HydratedPosition {
209 address: position_address,
210 data: position.clone(),
211 token_program: token_account.token_program,
212 }));
213 }
214
215 if let Some(position_bundle) = position_bundle {
216 let position_bundle_address = position_bundle_addresses[i];
217 let positions = bundled_positions_map
218 .get(&position_bundle.position_bundle_mint)
219 .unwrap_or(&Vec::new())
220 .iter()
221 .map(|x| HydratedBundledPosition {
222 address: x.0,
223 data: x.1.clone(),
224 })
225 .collect();
226 position_or_bundles.push(PositionOrBundle::PositionBundle(HydratedPositionBundle {
227 address: position_bundle_address,
228 data: position_bundle.clone(),
229 positions,
230 token_program: token_account.token_program,
231 }));
232 }
233 }
234
235 Ok(position_or_bundles)
236}
237
238pub async fn fetch_positions_in_fusion_pool(rpc: &RpcClient, fusion_pool: Pubkey) -> Result<Vec<DecodedAccount<Position>>, Box<dyn Error>> {
283 let filters = vec![PositionFilter::FusionPool(fusion_pool)];
284 fetch_all_position_with_filter(rpc, filters).await
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::tests::{setup_ata_with_amount, setup_fusion_pool, setup_mint_with_decimals, setup_position, setup_position_bundle, RpcContext};
291 use serial_test::serial;
292 use solana_program_test::tokio;
293 use solana_signer::Signer;
294 use std::error::Error;
295
296 #[tokio::test]
297 #[serial]
298 #[ignore = "Skipped until solana-bankrun supports gpa"]
299 async fn test_fetch_positions_for_owner_no_positions() -> Result<(), Box<dyn Error>> {
300 let ctx = RpcContext::new().await;
301 let owner = ctx.signer.pubkey();
302 let positions = fetch_positions_for_owner(&ctx.rpc, owner).await?;
303 assert!(positions.is_empty(), "No positions should exist for a new owner");
304 Ok(())
305 }
306
307 #[tokio::test]
308 #[serial]
309 #[ignore = "Skipped until solana-bankrun supports gpa"]
310 async fn test_fetch_positions_for_owner_with_position() -> Result<(), Box<dyn Error>> {
311 let ctx = RpcContext::new().await;
312 let mint_a = setup_mint_with_decimals(&ctx, 9).await?;
313 let mint_b = setup_mint_with_decimals(&ctx, 9).await?;
314 setup_ata_with_amount(&ctx, mint_a, 1_000_000_000).await?;
315 setup_ata_with_amount(&ctx, mint_b, 1_000_000_000).await?;
316
317 let fusion_pool = setup_fusion_pool(&ctx, mint_a, mint_b, 64, 300).await?;
318
319 let te_position_pubkey = setup_position(&ctx, fusion_pool, None, None).await?;
321
322 let _position_bundle_pubkey = setup_position_bundle(fusion_pool, Some(vec![(), ()])).await?;
324
325 let owner = ctx.signer.pubkey();
326 let positions = fetch_positions_for_owner(&ctx.rpc, owner).await?;
327
328 assert!(positions.len() >= 2, "Did not find all positions for the owner (expected normal, te_position, bundle)");
330
331 match &positions[0] {
333 PositionOrBundle::Position(pos) => {
334 assert_eq!(pos.address, te_position_pubkey);
335 }
336 _ => panic!("Expected a single position, but found a bundle!"),
337 }
338
339 Ok(())
340 }
341
342 #[tokio::test]
343 #[serial]
344 #[ignore = "Skipped until solana-bankrun supports gpa"]
345 async fn test_fetch_positions_in_fusion_pool() -> Result<(), Box<dyn Error>> {
346 let ctx = RpcContext::new().await;
347 let mint_a = setup_mint_with_decimals(&ctx, 9).await?;
348 let mint_b = setup_mint_with_decimals(&ctx, 9).await?;
349 setup_ata_with_amount(&ctx, mint_a, 1_000_000_000).await?;
350 setup_ata_with_amount(&ctx, mint_b, 1_000_000_000).await?;
351
352 let fusion_pool = setup_fusion_pool(&ctx, mint_a, mint_b, 64, 300).await?;
353
354 let _te_position_pubkey = setup_position(&ctx, fusion_pool, None, None).await?;
356
357 let _position_bundle_pubkey = setup_position_bundle(fusion_pool, Some(vec![(), ()])).await?;
359
360 let positions = fetch_positions_in_fusion_pool(&ctx.rpc, fusion_pool).await?;
361
362 assert!(positions.len() >= 2, "Should find multiple positions in this fusion_pool, including te_position & bundle");
364
365 Ok(())
366 }
367}