From 94ee5a6569b4aa438299db3aa555250e8a93c520 Mon Sep 17 00:00:00 2001 From: Jacob Gadikian Date: Wed, 1 Jan 2025 02:21:25 +0700 Subject: [PATCH] refactoring form idl --- Cargo.lock | 1 + packages/cli/src/main.rs | 81 ++++++----- packages/libcheese/Cargo.toml | 1 + packages/libcheese/src/common.rs | 15 ++ packages/libcheese/src/meteora.rs | 220 ++++++++++++++++++------------ packages/libcheese/src/solana.rs | 57 +++++++- 6 files changed, 245 insertions(+), 130 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d787c12..aba6880 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2122,6 +2122,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "bincode", + "lazy_static", "reqwest 0.12.12", "serde", "serde_json", diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 2bd5ac1..16aad40 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use clap::Parser; use libcheese::common::USDC_MINT; -use libcheese::common::{parse_other_token_name, CHEESE_MINT}; +use libcheese::common::{is_blacklisted, parse_other_token_name, CHEESE_MINT}; use libcheese::jupiter::fetch_jupiter_prices; use libcheese::meteora::{fetch_meteora_cheese_pools, MeteoraPool}; use libcheese::raydium::{fetch_raydium_cheese_pools, fetch_raydium_mint_ids}; @@ -16,6 +16,7 @@ use tokio::time; const SOL_PER_TX: f64 = 0.000005; // Approximate SOL cost per transaction const LOOP_INTERVAL: Duration = Duration::from_secs(30); const MIN_PROFIT_USD: f64 = 1.0; // Minimum profit in USD to execute trade +const MAX_USDC_INPUT: f64 = 10.0; // Maximum USDC input for any trade #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -40,12 +41,12 @@ struct DisplayPool { other_symbol: String, cheese_qty: String, other_qty: String, - pool_type: String, + pool_version: String, tvl: String, volume_usd: String, fee: String, pool_address: String, - cheese_price: String, // e.g. "$0.000057" + cheese_price: String, } #[derive(Debug, Default)] @@ -226,7 +227,7 @@ async fn run_iteration(executor: &Option) -> Result<()> { }; // Print table header - println!("\n| Source | Other Mint | Other Name | Pool Type | CHEESE Qty | Other Qty | Liquidity($) | Volume($) | Fee | CHEESE Price | Pool Address |"); + println!("\n| Source | Other Mint | Other Name | Version | CHEESE Qty | Other Qty | Liquidity($) | Volume($) | Fee | CHEESE Price | Pool Address |"); println!("|----------|----------------------------------------------|------------|------------|------------|-----------|--------------|-----------|-------|--------------|----------------------------------------------|"); // Prepare display pools @@ -242,6 +243,12 @@ async fn run_iteration(executor: &Option) -> Result<()> { }; let other_mint = pool.pool_token_mints[other_ix].clone(); + + // Skip blacklisted tokens + if is_blacklisted(&other_mint) { + continue; + } + let other_symbol = mint_to_symbol .get(&other_mint) .cloned() @@ -258,14 +265,14 @@ async fn run_iteration(executor: &Option) -> Result<()> { display_pools.push(DisplayPool { source: "Meteora".to_string(), - other_mint, - other_symbol, + other_mint: other_mint.to_string(), + other_symbol: other_symbol.to_string(), cheese_qty: format!("{:.2}", cheese_qty), other_qty: format!("{:.2}", other_qty), - pool_type: pool.pool_type.clone(), + pool_version: pool.pool_version.to_string(), tvl: format!("{:.2}", pool.pool_tvl), volume_usd: format!("{:.2}", pool.daily_volume), - fee: format!("{}%", pool.total_fee_pct.trim_end_matches('%')), + fee: format!("{:.2}%", pool.total_fee_pct.trim_end_matches('%')), pool_address: pool.pool_address.clone(), cheese_price: format!("${:.6}", cheese_usdc_price), }); @@ -302,7 +309,7 @@ async fn run_iteration(executor: &Option) -> Result<()> { other_symbol, cheese_qty: format!("{:.2}", cheese_qty), other_qty: format!("{:.2}", other_qty), - pool_type: pool.r#type.clone(), + pool_version: pool.r#type.clone(), tvl: format!("{:.2}", pool.tvl), volume_usd: format!("{:.2}", pool.day.volume), fee: format!("{:.2}%", pool.feeRate * 100.0), @@ -348,7 +355,7 @@ async fn run_iteration(executor: &Option) -> Result<()> { pool.source, pool.other_mint, pool.other_symbol, - pool.pool_type, + pool.pool_version, pool.cheese_qty, pool.other_qty, format!("{:.2}", tvl), @@ -390,12 +397,7 @@ async fn run_iteration(executor: &Option) -> Result<()> { .iter() .find(|p| p.pool_address == opp.pool_address) .unwrap(); - let fee_percent = pool - .total_fee_pct - .trim_end_matches('%') - .parse::() - .unwrap() - / 100.0; + let fee_percent = pool.total_fee_pct.trim_end_matches('%').parse::()? / 100.0; println!("\nPool: {} ({})", opp.pool_address, opp.symbol); println!("├─ Implied CHEESE price: ${:.10}", opp.implied_price); @@ -581,49 +583,56 @@ async fn run_iteration(executor: &Option) -> Result<()> { } fn find_arbitrage_opportunities( - pools: &[MeteoraPool], - mut cheese_usdc_price: f64, + meteora_pools: &[MeteoraPool], + cheese_usdc_price: f64, ) -> Result> { let mut opportunities = Vec::new(); - for pool in pools { - // Skip pools with derived prices - if pool.derived { - continue; - } - + for pool in meteora_pools { let (cheese_ix, other_ix) = if pool.pool_token_mints[0] == CHEESE_MINT { (0, 1) } else { (1, 0) }; - let cheese_qty: f64 = pool.pool_token_amounts[cheese_ix].parse()?; - let other_qty: f64 = pool.pool_token_amounts[other_ix].parse()?; - let is_usdc_pool = pool.pool_token_mints.contains(&USDC_MINT.to_string()); + let other_mint = &pool.pool_token_mints[other_ix]; - // If this is the USDC pool, use it to set the CHEESE price - if is_usdc_pool { - cheese_usdc_price = other_qty / cheese_qty; // USDC is worth $1, so price = USDC/CHEESE + // Skip blacklisted tokens + if is_blacklisted(other_mint) { continue; } - let fee_percent: f64 = pool.total_fee_pct.trim_end_matches('%').parse::()? / 100.0; + let cheese_qty: f64 = pool.pool_token_amounts[cheese_ix].parse()?; + let other_qty: f64 = pool.pool_token_amounts[other_ix].parse()?; + + // If this is USDC, use price of 1.0 + let other_price = if other_mint == USDC_MINT { + 1.0 // USDC is always worth $1 + } else { + cheese_usdc_price + }; + + let implied_price = (other_qty * other_price) / cheese_qty; + let price_diff_pct = ((implied_price - cheese_usdc_price) / cheese_usdc_price) * 100.0; + + let fee_percent = pool.total_fee_pct.trim_end_matches('%').parse::()? / 100.0; if cheese_qty <= 0.0 || other_qty <= 0.0 { continue; } - let implied_price = (other_qty * cheese_usdc_price) / cheese_qty; - let price_diff_pct = ((implied_price - cheese_usdc_price) / cheese_usdc_price) * 100.0; - // If price difference is significant (>1%) if price_diff_pct.abs() > 1.0 { - let max_trade_size = if is_usdc_pool { + // Calculate max trade size based on pool liquidity + let pool_based_size = if pool.pool_token_mints.contains(&USDC_MINT.to_string()) { cheese_qty * 0.1 } else { cheese_qty * 0.05 - }; // 10% of pool liquidity + }; + + // Limit by MAX_USDC_INPUT + let max_trade_size = (MAX_USDC_INPUT / cheese_usdc_price).min(pool_based_size); + let price_diff_per_cheese = (implied_price - cheese_usdc_price).abs(); let gross_profit = max_trade_size * price_diff_per_cheese; diff --git a/packages/libcheese/Cargo.toml b/packages/libcheese/Cargo.toml index c20ebe6..9b89af1 100644 --- a/packages/libcheese/Cargo.toml +++ b/packages/libcheese/Cargo.toml @@ -15,3 +15,4 @@ spl-associated-token-account = "6.0.0" spl-token = "4.0.0" bincode = "1.3" base64 = "0.22.1" +lazy_static = "1.4.0" diff --git a/packages/libcheese/src/common.rs b/packages/libcheese/src/common.rs index 77af8ff..a93b1cc 100644 --- a/packages/libcheese/src/common.rs +++ b/packages/libcheese/src/common.rs @@ -1,9 +1,20 @@ +use lazy_static::lazy_static; use serde::de::{self, Deserializer}; use serde::Deserialize; +use std::collections::HashSet; pub const CHEESE_MINT: &str = "A3hzGcTxZNSc7744CWB2LR5Tt9VTtEaQYpP6nwripump"; pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; +lazy_static! { + pub static ref BLACKLISTED_MINTS: HashSet<&'static str> = { + let mut s = HashSet::new(); + s.insert("27VkFr6b6DHoR6hSYZjUDbwJsV6MPSFqPavXLg8nduHW"); // OSHO + // Add other problematic tokens here + s + }; +} + pub fn de_string_to_f64<'de, D>(deserializer: D) -> std::result::Result where D: Deserializer<'de>, @@ -30,3 +41,7 @@ pub fn parse_other_token_name(pool_name: &str) -> String { } pool_name.to_string() } + +pub fn is_blacklisted(mint: &str) -> bool { + BLACKLISTED_MINTS.contains(mint) +} diff --git a/packages/libcheese/src/meteora.rs b/packages/libcheese/src/meteora.rs index 14c2311..32077e9 100644 --- a/packages/libcheese/src/meteora.rs +++ b/packages/libcheese/src/meteora.rs @@ -32,7 +32,11 @@ pub async fn fetch_meteora_cheese_pools(client: &Client) -> Result, - pub pool_type: String, - pub total_fee_pct: String, + pub pool_token_amounts: Vec, + pub pool_version: u8, + pub vaults: Vec, - // Add these new fields for swap accounts - pub a_vault: String, - pub b_vault: String, - pub a_token_vault: String, - pub b_token_vault: String, - pub a_vault_lp_mint: String, - pub b_vault_lp_mint: String, - pub a_vault_lp: String, - pub b_vault_lp: String, - pub protocol_token_fee: String, - - #[allow(dead_code)] - unknown: bool, - #[allow(dead_code)] - permissioned: bool, + #[serde(default)] + pub pool_token_decimals: Vec, + #[serde(default)] + pub pool_token_prices: Vec, #[serde(deserialize_with = "de_string_to_f64")] pub pool_tvl: f64, @@ -87,10 +81,35 @@ pub struct MeteoraPool { #[serde(alias = "trading_volume")] pub daily_volume: f64, - pub pool_token_amounts: Vec, - #[serde(default)] pub derived: bool, + #[serde(default)] + pub unknown: bool, + #[serde(default)] + pub permissioned: bool, + + #[serde(default)] + pub fee_volume: f64, + + #[serde(rename = "fee_pct", default = "default_fee_pct")] + pub total_fee_pct: String, + + // Add vault-related fields + pub vault_a: String, + pub vault_b: String, + pub token_vault_a: String, + pub token_vault_b: String, + pub vault_lp_mint_a: String, + pub vault_lp_mint_b: String, + pub vault_lp_token_a: String, + pub vault_lp_token_b: String, + pub protocol_fee_token_a: String, + pub protocol_fee_token_b: String, +} + +// Add this function to provide a default fee percentage +fn default_fee_pct() -> String { + "0.3".to_string() } // Add helper methods to MeteoraPool @@ -103,11 +122,71 @@ impl MeteoraPool { pub fn get_token_program(&self) -> Pubkey { spl_token::ID } + + // Add this method to initialize vault fields from the vaults array + pub fn init_vault_fields(&mut self) -> Result<()> { + if self.vaults.len() < 2 { + return Err(anyhow!("Pool must have at least 2 vaults")); + } + + self.vault_a = self.vaults[0].clone(); + self.vault_b = self.vaults[1].clone(); + + // For now, use placeholder values for other vault fields + self.token_vault_a = "placeholder".to_string(); + self.token_vault_b = "placeholder".to_string(); + self.vault_lp_mint_a = "placeholder".to_string(); + self.vault_lp_mint_b = "placeholder".to_string(); + self.vault_lp_token_a = "placeholder".to_string(); + self.vault_lp_token_b = "placeholder".to_string(); + self.protocol_fee_token_a = "placeholder".to_string(); + self.protocol_fee_token_b = "placeholder".to_string(); + + Ok(()) + } + + // Update get_swap_accounts to use new field names + pub async fn get_swap_accounts( + &self, + user_source_token: Pubkey, + user_dest_token: Pubkey, + ) -> Result { + Ok(MeteoraSwapAccounts { + pool: self.pool_address.parse()?, + user_source_token, + user_destination_token: user_dest_token, + a_vault: self.vault_a.parse()?, + b_vault: self.vault_b.parse()?, + a_token_vault: self.token_vault_a.parse()?, + b_token_vault: self.token_vault_b.parse()?, + a_vault_lp_mint: self.vault_lp_mint_a.parse()?, + b_vault_lp_mint: self.vault_lp_mint_b.parse()?, + a_vault_lp: self.vault_lp_token_a.parse()?, + b_vault_lp: self.vault_lp_token_b.parse()?, + protocol_token_fee: self.protocol_fee_token_a.parse()?, + }) + } } // ----------------------------------- // Part 1: Data Models // ----------------------------------- +#[derive(Debug)] +pub struct MeteoraSwapAccounts { + pub pool: Pubkey, + pub user_source_token: Pubkey, + pub user_destination_token: Pubkey, + pub a_vault: Pubkey, + pub b_vault: Pubkey, + pub a_token_vault: Pubkey, + pub b_token_vault: Pubkey, + pub a_vault_lp_mint: Pubkey, + pub b_vault_lp_mint: Pubkey, + pub a_vault_lp: Pubkey, + pub b_vault_lp: Pubkey, + pub protocol_token_fee: Pubkey, +} + #[derive(Debug, Deserialize)] pub struct PaginatedPoolSearchResponse { data: Vec, @@ -115,6 +194,28 @@ pub struct PaginatedPoolSearchResponse { total_count: i32, } +#[derive(Debug, Deserialize, Clone, Serialize)] +pub struct MeteoraQuoteResponse { + pub pool_address: String, + pub input_mint: String, + pub output_mint: String, + pub in_amount: String, + pub out_amount: String, + pub fee_amount: String, + pub price_impact: String, +} + +#[derive(Debug, Serialize, Clone)] +struct MeteoraSwapRequest { + user_public_key: String, + quote_response: MeteoraQuoteResponse, +} + +#[derive(Debug, Deserialize)] +struct MeteoraSwapResponse { + transaction: String, +} + // ----------------------------------- // Trading // ----------------------------------- @@ -139,8 +240,14 @@ pub async fn get_meteora_quote( let in_amount_pool: f64 = pool.pool_token_amounts[in_idx].parse()?; let out_amount_pool: f64 = pool.pool_token_amounts[out_idx].parse()?; - // Calculate fee - let fee_pct: f64 = pool.total_fee_pct.trim_end_matches('%').parse::()? / 100.0; + // Default fee if not specified (0.3%) + let fee_pct = pool + .total_fee_pct + .trim_end_matches('%') + .parse::() + .unwrap_or(0.3) + / 100.0; + let amount_in_after_fee = amount_in as f64 * (1.0 - fee_pct); // Calculate out amount using constant product formula: (x + Δx)(y - Δy) = xy @@ -179,52 +286,13 @@ async fn fetch_pool_state(client: &Client, pool_address: &str) -> Result = resp.json().await?; - pools + let mut pool = pools .into_iter() .next() - .ok_or_else(|| anyhow!("Pool not found: {}", pool_address)) -} + .ok_or_else(|| anyhow!("Pool not found: {}", pool_address))?; -// Add these new structs to help with swap account setup -#[derive(Debug)] -pub struct MeteoraSwapAccounts { - pub pool: Pubkey, - pub user_source_token: Pubkey, - pub user_destination_token: Pubkey, - pub a_vault: Pubkey, - pub b_vault: Pubkey, - pub a_token_vault: Pubkey, - pub b_token_vault: Pubkey, - pub a_vault_lp_mint: Pubkey, - pub b_vault_lp_mint: Pubkey, - pub a_vault_lp: Pubkey, - pub b_vault_lp: Pubkey, - pub protocol_token_fee: Pubkey, -} - -impl MeteoraPool { - // Add this helper method to get swap accounts - pub async fn get_swap_accounts( - &self, - user_source_token: Pubkey, - user_dest_token: Pubkey, - ) -> Result { - // Parse pool data to get required accounts - Ok(MeteoraSwapAccounts { - pool: self.pool_address.parse()?, - user_source_token, - user_destination_token: user_dest_token, - a_vault: self.a_vault.parse()?, - b_vault: self.b_vault.parse()?, - a_token_vault: self.a_token_vault.parse()?, - b_token_vault: self.b_token_vault.parse()?, - a_vault_lp_mint: self.a_vault_lp_mint.parse()?, - b_vault_lp_mint: self.b_vault_lp_mint.parse()?, - a_vault_lp: self.a_vault_lp.parse()?, - b_vault_lp: self.b_vault_lp.parse()?, - protocol_token_fee: self.protocol_token_fee.parse()?, - }) - } + pool.init_vault_fields()?; + Ok(pool) } // Update get_meteora_swap_transaction to use proper accounts @@ -278,25 +346,3 @@ pub async fn get_meteora_swap_transaction( let swap: MeteoraSwapResponse = resp.json().await?; Ok(swap.transaction) } - -#[derive(Debug, Deserialize, Clone, Serialize)] -pub struct MeteoraQuoteResponse { - pub pool_address: String, - pub input_mint: String, - pub output_mint: String, - pub in_amount: String, - pub out_amount: String, - pub fee_amount: String, - pub price_impact: String, -} - -#[derive(Debug, Serialize, Clone)] -struct MeteoraSwapRequest { - user_public_key: String, - quote_response: MeteoraQuoteResponse, -} - -#[derive(Debug, Deserialize)] -struct MeteoraSwapResponse { - transaction: String, -} diff --git a/packages/libcheese/src/solana.rs b/packages/libcheese/src/solana.rs index 10ee921..7db2a8b 100644 --- a/packages/libcheese/src/solana.rs +++ b/packages/libcheese/src/solana.rs @@ -14,7 +14,7 @@ use spl_token; use std::{str::FromStr, time::Duration}; use tokio::time::sleep; -use crate::meteora::{self, MeteoraPool}; +use crate::meteora::{self, MeteoraPool, MeteoraSwapAccounts}; const MAX_RETRIES: u32 = 3; const RETRY_DELAY: Duration = Duration::from_secs(1); @@ -44,8 +44,15 @@ impl TradeExecutor { input_mint: &str, output_mint: &str, amount_in: u64, - slippage_bps: u64, + slippage_bps: u16, ) -> Result { + println!("Building trade with accounts:"); + println!("- Pool: {}", pool.pool_address); + println!("- Input mint: {}", input_mint); + println!("- Output mint: {}", output_mint); + println!("- Amount in: {}", amount_in); + // Add more detailed logging here... + // Check balance before trading self.check_token_balance(input_mint, amount_in).await?; @@ -92,6 +99,32 @@ impl TradeExecutor { let dest_token = get_associated_token_address(&self.wallet.pubkey(), &Pubkey::from_str(output_mint)?); + // Determine if we're swapping A->B or B->A to select correct protocol fee account + let is_a_to_b = pool.pool_token_mints[0] == input_mint; + let protocol_fee = if is_a_to_b { + pool.protocol_fee_token_a.clone() + } else { + pool.protocol_fee_token_b.clone() + }; + + // Log all accounts for debugging + println!("Swap accounts:"); + println!("1. pool: {}", pool.pool_address); + println!("2. userSourceToken: {}", source_token); + println!("3. userDestinationToken: {}", dest_token); + println!("4. aVault: {}", pool.vault_a); + println!("5. bVault: {}", pool.vault_b); + println!("6. aTokenVault: {}", pool.token_vault_a); + println!("7. bTokenVault: {}", pool.token_vault_b); + println!("8. aVaultLpMint: {}", pool.vault_lp_mint_a); + println!("9. bVaultLpMint: {}", pool.vault_lp_mint_b); + println!("10. aVaultLp: {}", pool.vault_lp_token_a); + println!("11. bVaultLp: {}", pool.vault_lp_token_b); + println!("12. protocolTokenFee: {}", protocol_fee); + println!("13. user: {}", self.wallet.pubkey()); + println!("14. vaultProgram: {}", pool.get_vault_program()?); + println!("15. tokenProgram: {}", spl_token::ID); + // 1. Get quote from Meteora let quote = meteora::get_meteora_quote( &self.http_client, @@ -102,15 +135,25 @@ impl TradeExecutor { ) .await?; - // 2. Get swap accounts - let swap_accounts = pool.get_swap_accounts(source_token, dest_token).await?; - - // 3. Get swap transaction + // 2. Get swap transaction let swap_tx = meteora::get_meteora_swap_transaction( &self.http_client, "e, &self.wallet.pubkey().to_string(), - &swap_accounts, + &MeteoraSwapAccounts { + pool: Pubkey::from_str(&pool.pool_address)?, + user_source_token: source_token, + user_destination_token: dest_token, + a_vault: Pubkey::from_str(&pool.vault_a)?, + b_vault: Pubkey::from_str(&pool.vault_b)?, + a_token_vault: Pubkey::from_str(&pool.token_vault_a)?, + b_token_vault: Pubkey::from_str(&pool.token_vault_b)?, + a_vault_lp_mint: Pubkey::from_str(&pool.vault_lp_mint_a)?, + b_vault_lp_mint: Pubkey::from_str(&pool.vault_lp_mint_b)?, + a_vault_lp: Pubkey::from_str(&pool.vault_lp_token_a)?, + b_vault_lp: Pubkey::from_str(&pool.vault_lp_token_b)?, + protocol_token_fee: Pubkey::from_str(&protocol_fee)?, + }, ) .await?;