use crate::graph::{ArbGraph, PoolEdge};
use anyhow::Result;
use scematica_core::{rpc::RpcConnection, types::DexKind};
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use std::sync::Arc;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolJson {
pub address: String,
pub dex: String,
pub token_a_mint: String,
pub token_b_mint: String,
pub token_a_vault: String,
pub token_b_vault: String,
pub fee_numerator: u64,
pub fee_denominator: u64,
}
pub async fn load_pools_from_dir(
dir: &str,
graph: &ArbGraph,
rpc: &Arc<RpcConnection>,
) -> Result<usize> {
let mut count = 0;
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(e) => {
warn!("Pool dir '{}' not found: {}. Starting with empty graph.", dir, e);
return Ok(0);
}
};
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let sub_dir = path.to_string_lossy().to_string();
count += Box::pin(load_pools_from_dir(&sub_dir, graph, rpc)).await?;
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let content = std::fs::read_to_string(&path)?;
match serde_json::from_str::<PoolJson>(&content) {
Ok(pool_json) => {
if let Some(edge) = pool_json_to_edge(&pool_json, rpc).await {
let mint_a: Pubkey = pool_json.token_a_mint.parse().unwrap_or_default();
let mint_b: Pubkey = pool_json.token_b_mint.parse().unwrap_or_default();
graph.add_pool(mint_a, mint_b, edge);
count += 1;
}
}
Err(e) => {
warn!("Failed to parse pool JSON {:?}: {}", path, e);
}
}
}
info!("Loaded {} pools from {}", count, dir);
Ok(count)
}
async fn pool_json_to_edge(pool: &PoolJson, rpc: &Arc<RpcConnection>) -> Option<PoolEdge> {
let pool_address: Pubkey = pool.address.parse().ok()?;
let vault_a: Pubkey = pool.token_a_vault.parse().ok()?;
let vault_b: Pubkey = pool.token_b_vault.parse().ok()?;
let dex = match pool.dex.as_str() {
"raydium" | "Raydium" => DexKind::Raydium,
"orca" | "Orca" => DexKind::Orca,
"meteora" | "Meteora" => DexKind::Meteora,
"saber" | "Saber" => DexKind::Saber,
"mercurial" | "Mercurial" => DexKind::Mercurial,
_ => DexKind::Unknown,
};
let (reserve_a, reserve_b) = fetch_reserves(rpc, &vault_a, &vault_b).await;
Some(PoolEdge {
pool_address,
dex,
reserve_a,
reserve_b,
fee_numerator: pool.fee_numerator,
fee_denominator: pool.fee_denominator,
})
}
async fn fetch_reserves(
rpc: &Arc<RpcConnection>,
vault_a: &Pubkey,
vault_b: &Pubkey,
) -> (u64, u64) {
let (res_a, res_b) = tokio::join!(
rpc.client.get_token_account_balance(vault_a),
rpc.client.get_token_account_balance(vault_b),
);
let reserve_a = res_a
.ok()
.and_then(|b| b.amount.parse::<u64>().ok())
.unwrap_or(0);
let reserve_b = res_b
.ok()
.and_then(|b| b.amount.parse::<u64>().ok())
.unwrap_or(0);
(reserve_a, reserve_b)
}
pub async fn refresh_graph_reserves(
_graph: &ArbGraph,
pool_vaults: &[(Pubkey, Pubkey, Pubkey)],
rpc: &Arc<RpcConnection>,
) -> Result<()> {
let all_vaults: Vec<Pubkey> = pool_vaults
.iter()
.flat_map(|(_, va, vb)| [*va, *vb])
.collect();
for chunk in all_vaults.chunks(100) {
let accounts = rpc.client.get_multiple_accounts(chunk).await?;
debug!("Refreshed {} vault accounts", accounts.len());
}
Ok(())
}