use fusionamm_client::{
fetch_all_position_with_filter, get_bundled_position_address, get_position_address, get_position_bundle_address, DecodedAccount, Position,
PositionBundle, PositionFilter,
};
use fusionamm_core::POSITION_BUNDLE_SIZE;
use solana_account::Account;
use solana_client::{nonblocking::rpc_client::RpcClient, rpc_request::TokenAccountsFilter};
use solana_pubkey::Pubkey;
use std::{collections::HashMap, error::Error};
use crate::{get_token_accounts_for_owner, ParsedTokenAccount};
#[derive(Debug)]
pub struct HydratedPosition {
pub address: Pubkey,
pub data: Position,
pub token_program: Pubkey,
}
#[derive(Debug)]
pub struct HydratedBundledPosition {
pub address: Pubkey,
pub data: Position,
}
#[derive(Debug)]
pub struct HydratedPositionBundle {
pub address: Pubkey,
pub data: PositionBundle,
pub positions: Vec<HydratedBundledPosition>,
pub token_program: Pubkey,
}
#[derive(Debug)]
pub enum PositionOrBundle {
Position(HydratedPosition),
PositionBundle(HydratedPositionBundle),
}
fn get_position_in_bundle_addresses(position_bundle: &PositionBundle) -> Vec<Pubkey> {
let mut positions: Vec<Pubkey> = Vec::new();
for i in 0..POSITION_BUNDLE_SIZE {
let byte_index = i / 8;
let bit_index = i % 8;
if position_bundle.position_bitmap[byte_index] & (1 << bit_index) != 0 {
let result = get_bundled_position_address(&position_bundle.position_bundle_mint, i as u8);
if let Ok(result) = result {
positions.push(result.0);
}
}
}
positions
}
pub async fn fetch_positions_for_owner(rpc: &RpcClient, owner: Pubkey) -> Result<Vec<PositionOrBundle>, Box<dyn Error>> {
let token_accounts = get_token_accounts_for_owner(rpc, owner, TokenAccountsFilter::ProgramId(spl_token::ID)).await?;
let token_extension_accounts = get_token_accounts_for_owner(rpc, owner, TokenAccountsFilter::ProgramId(spl_token_2022::ID)).await?;
let potiential_tokens: Vec<ParsedTokenAccount> = [token_accounts, token_extension_accounts]
.into_iter()
.flatten()
.filter(|x| x.amount == 1)
.collect();
let position_addresses: Vec<Pubkey> = potiential_tokens
.iter()
.map(|x| get_position_address(&x.mint).map(|x| x.0))
.collect::<Result<Vec<Pubkey>, _>>()?;
let position_bundle_addresses: Vec<Pubkey> = potiential_tokens
.iter()
.map(|x| get_position_bundle_address(&x.mint).map(|x| x.0))
.collect::<Result<Vec<Pubkey>, _>>()?;
let position_infos = rpc.get_multiple_accounts(&position_addresses).await?;
let positions: Vec<Option<Position>> = position_infos
.iter()
.map(|x| x.as_ref().and_then(|x| Position::from_bytes(&x.data).ok()))
.collect();
let position_bundle_infos = rpc.get_multiple_accounts(&position_bundle_addresses).await?;
let position_bundles: Vec<Option<PositionBundle>> = position_bundle_infos
.iter()
.map(|x| x.as_ref().and_then(|x| PositionBundle::from_bytes(&x.data).ok()))
.collect();
let bundled_positions_addresses: Vec<Pubkey> = position_bundles.iter().flatten().flat_map(get_position_in_bundle_addresses).collect();
let bundled_positions_infos: Vec<Account> = rpc
.get_multiple_accounts(&bundled_positions_addresses)
.await?
.into_iter()
.flatten()
.collect();
let mut bundled_positions_map: HashMap<Pubkey, Vec<(Pubkey, Position)>> = HashMap::new();
for i in 0..bundled_positions_addresses.len() {
let bundled_position_address = bundled_positions_addresses[i];
let bundled_position_info = &bundled_positions_infos[i];
let position = Position::from_bytes(&bundled_position_info.data)?;
let key = position.position_mint;
bundled_positions_map.entry(key).or_default();
if let Some(x) = bundled_positions_map.get_mut(&key) {
x.push((bundled_position_address, position))
}
}
let mut position_or_bundles: Vec<PositionOrBundle> = Vec::new();
for i in 0..potiential_tokens.len() {
let position = &positions[i];
let position_bundle = &position_bundles[i];
let token_account = &potiential_tokens[i];
if let Some(position) = position {
let position_address = position_addresses[i];
position_or_bundles.push(PositionOrBundle::Position(HydratedPosition {
address: position_address,
data: position.clone(),
token_program: token_account.token_program,
}));
}
if let Some(position_bundle) = position_bundle {
let position_bundle_address = position_bundle_addresses[i];
let positions = bundled_positions_map
.get(&position_bundle.position_bundle_mint)
.unwrap_or(&Vec::new())
.iter()
.map(|x| HydratedBundledPosition {
address: x.0,
data: x.1.clone(),
})
.collect();
position_or_bundles.push(PositionOrBundle::PositionBundle(HydratedPositionBundle {
address: position_bundle_address,
data: position_bundle.clone(),
positions,
token_program: token_account.token_program,
}));
}
}
Ok(position_or_bundles)
}
pub async fn fetch_positions_in_fusion_pool(rpc: &RpcClient, fusion_pool: Pubkey) -> Result<Vec<DecodedAccount<Position>>, Box<dyn Error>> {
let filters = vec![PositionFilter::FusionPool(fusion_pool)];
fetch_all_position_with_filter(rpc, filters).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{setup_ata_with_amount, setup_fusion_pool, setup_mint_with_decimals, setup_position, setup_position_bundle, RpcContext};
use serial_test::serial;
use solana_program_test::tokio;
use solana_signer::Signer;
use std::error::Error;
#[tokio::test]
#[serial]
#[ignore = "Skipped until solana-bankrun supports gpa"]
async fn test_fetch_positions_for_owner_no_positions() -> Result<(), Box<dyn Error>> {
let ctx = RpcContext::new().await;
let owner = ctx.signer.pubkey();
let positions = fetch_positions_for_owner(&ctx.rpc, owner).await?;
assert!(positions.is_empty(), "No positions should exist for a new owner");
Ok(())
}
#[tokio::test]
#[serial]
#[ignore = "Skipped until solana-bankrun supports gpa"]
async fn test_fetch_positions_for_owner_with_position() -> Result<(), Box<dyn Error>> {
let ctx = RpcContext::new().await;
let mint_a = setup_mint_with_decimals(&ctx, 9).await?;
let mint_b = setup_mint_with_decimals(&ctx, 9).await?;
setup_ata_with_amount(&ctx, mint_a, 1_000_000_000).await?;
setup_ata_with_amount(&ctx, mint_b, 1_000_000_000).await?;
let fusion_pool = setup_fusion_pool(&ctx, mint_a, mint_b, 64, 300).await?;
let te_position_pubkey = setup_position(&ctx, fusion_pool, None, None).await?;
let _position_bundle_pubkey = setup_position_bundle(fusion_pool, Some(vec![(), ()])).await?;
let owner = ctx.signer.pubkey();
let positions = fetch_positions_for_owner(&ctx.rpc, owner).await?;
assert!(positions.len() >= 2, "Did not find all positions for the owner (expected normal, te_position, bundle)");
match &positions[0] {
PositionOrBundle::Position(pos) => {
assert_eq!(pos.address, te_position_pubkey);
}
_ => panic!("Expected a single position, but found a bundle!"),
}
Ok(())
}
#[tokio::test]
#[serial]
#[ignore = "Skipped until solana-bankrun supports gpa"]
async fn test_fetch_positions_in_fusion_pool() -> Result<(), Box<dyn Error>> {
let ctx = RpcContext::new().await;
let mint_a = setup_mint_with_decimals(&ctx, 9).await?;
let mint_b = setup_mint_with_decimals(&ctx, 9).await?;
setup_ata_with_amount(&ctx, mint_a, 1_000_000_000).await?;
setup_ata_with_amount(&ctx, mint_b, 1_000_000_000).await?;
let fusion_pool = setup_fusion_pool(&ctx, mint_a, mint_b, 64, 300).await?;
let _te_position_pubkey = setup_position(&ctx, fusion_pool, None, None).await?;
let _position_bundle_pubkey = setup_position_bundle(fusion_pool, Some(vec![(), ()])).await?;
let positions = fetch_positions_in_fusion_pool(&ctx.rpc, fusion_pool).await?;
assert!(positions.len() >= 2, "Should find multiple positions in this fusion_pool, including te_position & bundle");
Ok(())
}
}