diff --git a/Cargo.lock b/Cargo.lock index 94bc5de..aba6880 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -766,7 +766,7 @@ dependencies = [ "anyhow", "clap", "libcheese", - "reqwest 0.12.11", + "reqwest 0.12.12", "serde", "serde_json", "solana-sdk", @@ -2122,12 +2122,14 @@ dependencies = [ "anyhow", "base64 0.22.1", "bincode", - "reqwest 0.12.11", + "lazy_static", + "reqwest 0.12.12", "serde", "serde_json", "solana-client", "solana-sdk", "spl-associated-token-account", + "spl-token 4.0.2", "tokio", ] @@ -2977,9 +2979,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.11" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe060fe50f524be480214aba758c71f99f90ee8c83c5a36b5e9e1d568eb4eb3" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64 0.22.1", "bytes", @@ -4982,6 +4984,21 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "spl-token" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9e171cbcb4b1f72f6d78ed1e975cb467f56825c27d09b8dd2608e4e7fc8b3b" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "thiserror 1.0.69", +] + [[package]] name = "spl-token" version = "6.0.0" @@ -6156,9 +6173,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "e6f5bb5257f2407a5425c6e749bfd9692192a73e70a6060516ac04f889087d68" dependencies = [ "memchr", ] diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 7161309..16aad40 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -1,25 +1,22 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use clap::Parser; -use libcheese::common::{parse_other_token_name, CHEESE_MINT}; +use libcheese::common::USDC_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}; use libcheese::solana::TradeExecutor; use reqwest::Client; -use solana_sdk::signature::Keypair; use solana_sdk::signer::keypair::read_keypair_file; +use solana_sdk::signer::Signer; use std::collections::{HashMap, HashSet}; -use std::env; -use std::str::FromStr; use std::time::Duration; use tokio::time; -const WALLET_CHEESE_BALANCE: f64 = 5_000_000.0; -const WALLET_SOL_BALANCE: f64 = 1.0; const SOL_PER_TX: f64 = 0.000005; // Approximate SOL cost per transaction -const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; 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)] @@ -44,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)] @@ -121,13 +118,43 @@ async fn main() -> Result<()> { let keypair_path = args.keypair.unwrap(); let keypair = read_keypair_file(&keypair_path) - .map_err(|e| anyhow::anyhow!("Failed to read keypair file: {}", e))?; + .map_err(|e| anyhow!("Failed to read keypair file: {}", e))?; + + println!("\n=== Wallet Information ==="); + println!("Address: {}", keypair.pubkey()); let rpc_url = args .rpc_url .unwrap_or_else(|| "https://api.mainnet-beta.solana.com".to_string()); - Some(TradeExecutor::new(&rpc_url, keypair)) + let executor = TradeExecutor::new(&rpc_url, keypair); + + // Get and display SOL balance + let sol_balance = executor + .rpc_client + .get_balance(&executor.wallet.pubkey()) + .map_err(|e| anyhow!("Failed to get SOL balance: {}", e))?; + println!("SOL balance: {} SOL", sol_balance as f64 / 1_000_000_000.0); + + // Get and display USDC balance + let usdc_balance = match executor.get_token_balance(&USDC_MINT.parse()?).await { + Ok(balance) => balance, + Err(_) => 0, + }; + println!("USDC balance: {} USDC", usdc_balance as f64 / 1_000_000.0); + + // Get and display CHEESE balance + let cheese_balance = match executor.get_token_balance(&CHEESE_MINT.parse()?).await { + Ok(balance) => balance, + Err(_) => 0, + }; + println!( + "CHEESE balance: {} CHEESE", + cheese_balance as f64 / 1_000_000.0 + ); + println!("=====================\n"); + + Some(executor) } else { None }; @@ -200,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 @@ -216,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() @@ -232,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), }); @@ -276,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), @@ -322,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), @@ -364,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); @@ -414,7 +442,15 @@ async fn run_iteration(executor: &Option) -> Result<()> { // Execute trade if in hot mode if let Some(executor) = executor { - println!("\nExecuting trade..."); + println!("\n=== Starting Trade Execution ==="); + println!("Trade details:"); + println!("- Is sell: {}", opp.is_sell); + println!("- Max trade size: {}", opp.max_trade_size); + println!("- USDC price: {}", opp.usdc_price); + println!("- Implied price: {}", opp.implied_price); + println!("- Net profit USD: {}", opp.net_profit_usd); + println!("- Pool address: {}", opp.pool_address); + println!("- Symbol: {}", opp.symbol); // Get the other token's index let (_, other_ix) = if pool.pool_token_mints[0] == CHEESE_MINT { @@ -422,12 +458,26 @@ async fn run_iteration(executor: &Option) -> Result<()> { } else { (1, 0) }; + println!("\nPool details:"); + println!("- Pool token mints: {:?}", pool.pool_token_mints); + println!("- Pool token amounts: {:?}", pool.pool_token_amounts); + println!("- Other token index: {}", other_ix); + + // Ensure all necessary token accounts exist before trading + println!("\nEnsuring token accounts exist..."); + executor.ensure_token_account(USDC_MINT).await?; + executor.ensure_token_account(CHEESE_MINT).await?; + executor + .ensure_token_account(&pool.pool_token_mints[other_ix]) + .await?; if opp.is_sell { - // Path: USDC -> CHEESE -> Target -> CHEESE -> USDC + println!("\nExecuting sell path: USDC -> CHEESE -> Target -> CHEESE -> USDC"); // 1. USDC -> CHEESE on Meteora - let amount_in_usdc = (opp.max_trade_size * opp.usdc_price * 1_000_000.0) as u64; + let amount_in_usdc = ((opp.max_trade_size * opp.usdc_price) as u64) * 1_000_000; // Convert to USDC lamports (6 decimals) + println!("\nStep 1: USDC -> CHEESE"); + println!("Amount in USDC: {}", amount_in_usdc as f64 / 1_000_000.0); // Display in human-readable USDC let sig1 = executor .execute_trade( usdc_pool, @@ -441,6 +491,8 @@ async fn run_iteration(executor: &Option) -> Result<()> { // 2. CHEESE -> Target token let amount_in_cheese = (opp.max_trade_size * 1_000_000_000.0) as u64; + println!("\nStep 2: CHEESE -> {}", opp.symbol); + println!("Amount in CHEESE: {}", amount_in_cheese); let sig2 = executor .execute_trade( pool, @@ -454,6 +506,8 @@ async fn run_iteration(executor: &Option) -> Result<()> { // 3. Target -> CHEESE let amount_in_target = (opp.other_qty * 0.1 * 1_000_000_000.0) as u64; // 10% of target token liquidity + println!("\nStep 3: {} -> CHEESE", opp.symbol); + println!("Amount in {}: {}", opp.symbol, amount_in_target); let sig3 = executor .execute_trade( pool, @@ -466,15 +520,19 @@ async fn run_iteration(executor: &Option) -> Result<()> { println!("3. {} -> CHEESE: {}", opp.symbol, sig3); // 4. CHEESE -> USDC + println!("\nStep 4: CHEESE -> USDC"); + println!("Amount in CHEESE: {}", amount_in_cheese); let sig4 = executor .execute_trade(usdc_pool, CHEESE_MINT, USDC_MINT, amount_in_cheese, 50) .await?; println!("4. CHEESE -> USDC: {}", sig4); } else { - // Path: USDC -> CHEESE -> Target -> CHEESE -> USDC + println!("\nExecuting buy path: USDC -> CHEESE -> Target -> CHEESE -> USDC"); // 1. USDC -> CHEESE on Meteora let amount_in_usdc = (opp.max_trade_size * opp.usdc_price * 1_000_000.0) as u64; + println!("\nStep 1: USDC -> CHEESE"); + println!("Amount in USDC: {}", amount_in_usdc); let sig1 = executor .execute_trade(usdc_pool, USDC_MINT, CHEESE_MINT, amount_in_usdc, 50) .await?; @@ -482,6 +540,8 @@ async fn run_iteration(executor: &Option) -> Result<()> { // 2. CHEESE -> Target token let amount_in_cheese = (opp.max_trade_size * 1_000_000_000.0) as u64; + println!("\nStep 2: CHEESE -> {}", opp.symbol); + println!("Amount in CHEESE: {}", amount_in_cheese); let sig2 = executor .execute_trade( pool, @@ -495,6 +555,8 @@ async fn run_iteration(executor: &Option) -> Result<()> { // 3. Target -> CHEESE let amount_in_target = (opp.other_qty * 0.1 * 1_000_000_000.0) as u64; // 10% of target token liquidity + println!("\nStep 3: {} -> CHEESE", opp.symbol); + println!("Amount in {}: {}", opp.symbol, amount_in_target); let sig3 = executor .execute_trade( pool, @@ -507,6 +569,8 @@ async fn run_iteration(executor: &Option) -> Result<()> { println!("3. {} -> CHEESE: {}", opp.symbol, sig3); // 4. CHEESE -> USDC + println!("\nStep 4: CHEESE -> USDC"); + println!("Amount in CHEESE: {}", amount_in_cheese); let sig4 = executor .execute_trade(usdc_pool, CHEESE_MINT, USDC_MINT, amount_in_cheese, 50) .await?; @@ -519,37 +583,56 @@ async fn run_iteration(executor: &Option) -> Result<()> { } fn find_arbitrage_opportunities( - pools: &[MeteoraPool], + meteora_pools: &[MeteoraPool], cheese_usdc_price: f64, ) -> Result> { let mut opportunities = Vec::new(); - for pool in pools { - // Skip USDC pool and pools with derived prices - if pool.pool_address == "2rkTh46zo8wUvPJvACPTJ16RNUHEM9EZ1nLYkUxZEHkw" || 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 other_mint = &pool.pool_token_mints[other_ix]; + + // Skip blacklisted tokens + if is_blacklisted(other_mint) { + continue; + } + let cheese_qty: f64 = pool.pool_token_amounts[cheese_ix].parse()?; let other_qty: f64 = pool.pool_token_amounts[other_ix].parse()?; - let fee_percent: f64 = pool.total_fee_pct.trim_end_matches('%').parse::()? / 100.0; + + // 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 = cheese_qty * 0.1; // 10% of pool liquidity + // 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 + }; + + // 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 ec3080f..9b89af1 100644 --- a/packages/libcheese/Cargo.toml +++ b/packages/libcheese/Cargo.toml @@ -12,5 +12,7 @@ anyhow = "1.0" solana-sdk = "2.1.7" solana-client = "2.1.7" 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 b0006fe..a93b1cc 100644 --- a/packages/libcheese/src/common.rs +++ b/packages/libcheese/src/common.rs @@ -1,7 +1,19 @@ +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 @@ -29,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 87c0efa..32077e9 100644 --- a/packages/libcheese/src/meteora.rs +++ b/packages/libcheese/src/meteora.rs @@ -2,6 +2,8 @@ use crate::common::{de_string_to_f64, CHEESE_MINT}; use anyhow::{anyhow, Result}; use reqwest::Client; use serde::{Deserialize, Serialize}; +use serde_json::json; +use solana_sdk::pubkey::Pubkey; // ----------------------------------- // Networking @@ -30,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, - // For demonstration, we won't read these - #[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, @@ -75,15 +81,112 @@ 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 +impl MeteoraPool { + pub fn get_vault_program(&self) -> Result { + // This is Meteora's vault program ID + Ok("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi".parse()?) + } + + 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, @@ -91,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 // ----------------------------------- @@ -115,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 @@ -155,56 +286,63 @@ 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))?; + + pool.init_vault_fields()?; + Ok(pool) } +// Update get_meteora_swap_transaction to use proper accounts pub async fn get_meteora_swap_transaction( client: &Client, quote: &MeteoraQuoteResponse, user_pubkey: &str, + swap_accounts: &MeteoraSwapAccounts, ) -> Result { - let base_url = "https://amm-v2.meteora.ag"; - let swap_url = format!("{}/swap", base_url); - - let swap_request = MeteoraSwapRequest { - user_public_key: user_pubkey.to_string(), - quote_response: quote.clone(), - }; + let swap_url = "https://amm-v2.meteora.ag/swap"; + + let swap_request = json!({ + "user_public_key": user_pubkey, + "quote_response": quote, + "accounts": { + "pool": swap_accounts.pool.to_string(), + "userSourceToken": swap_accounts.user_source_token.to_string(), + "userDestinationToken": swap_accounts.user_destination_token.to_string(), + "aVault": swap_accounts.a_vault.to_string(), + "bVault": swap_accounts.b_vault.to_string(), + "aTokenVault": swap_accounts.a_token_vault.to_string(), + "bTokenVault": swap_accounts.b_token_vault.to_string(), + "aVaultLpMint": swap_accounts.a_vault_lp_mint.to_string(), + "bVaultLpMint": swap_accounts.b_vault_lp_mint.to_string(), + "aVaultLp": swap_accounts.a_vault_lp.to_string(), + "bVaultLp": swap_accounts.b_vault_lp.to_string(), + "protocolTokenFee": swap_accounts.protocol_token_fee.to_string(), + "user": user_pubkey, + "vaultProgram": "24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi", + "tokenProgram": spl_token::ID.to_string() + } + }); - // Log the swap request - println!("Sending Meteora swap request: {:?}", swap_request); + println!( + "Sending swap request: {}", + serde_json::to_string_pretty(&swap_request)? + ); - let resp = client.post(&swap_url).json(&swap_request).send().await?; + let resp = client.post(swap_url).json(&swap_request).send().await?; if !resp.status().is_success() { - return Err(anyhow!("Meteora swap request failed: {}", resp.status())); + let status = resp.status(); + let error_text = resp.text().await?; + return Err(anyhow!( + "Meteora swap request failed: {} - {}", + status, + error_text + )); } 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 bdad1e7..7db2a8b 100644 --- a/packages/libcheese/src/solana.rs +++ b/packages/libcheese/src/solana.rs @@ -9,17 +9,19 @@ use solana_sdk::{ signer::Signer, transaction::Transaction, }; +use spl_associated_token_account::get_associated_token_address; +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); pub struct TradeExecutor { - rpc_client: RpcClient, - wallet: Keypair, + pub rpc_client: RpcClient, + pub wallet: Keypair, http_client: Client, } @@ -42,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?; @@ -84,6 +93,38 @@ impl TradeExecutor { output_mint: &str, amount_in: u64, ) -> Result { + // Get source and destination token accounts + let source_token = + get_associated_token_address(&self.wallet.pubkey(), &Pubkey::from_str(input_mint)?); + 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, @@ -94,23 +135,43 @@ impl TradeExecutor { ) .await?; - println!( - "Got quote: {} -> {} ({} -> {})", - input_mint, output_mint, quote.in_amount, quote.out_amount - ); - // 2. Get swap transaction let swap_tx = meteora::get_meteora_swap_transaction( &self.http_client, "e, &self.wallet.pubkey().to_string(), + &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?; - // 3. Deserialize and sign transaction - let tx: Transaction = bincode::deserialize(&base64::decode(swap_tx)?)?; + // 4. Deserialize and sign transaction + let mut tx: Transaction = bincode::deserialize(&base64::decode(swap_tx)?)?; - // 4. Simulate transaction with detailed error reporting + // Verify and update blockhash if needed + let recent_blockhash = self.rpc_client.get_latest_blockhash()?; + if tx.message.recent_blockhash != recent_blockhash { + tx.message.recent_blockhash = recent_blockhash; + } + + // Sign the transaction if not already signed + if tx.signatures.is_empty() || tx.signatures[0] == Signature::default() { + tx.sign(&[&self.wallet], tx.message.recent_blockhash); + } + + // 5. Simulate transaction with detailed error reporting match self.simulate_transaction(&tx).await { Ok(_) => println!("Transaction simulation successful"), Err(e) => { @@ -119,27 +180,72 @@ impl TradeExecutor { } } - // 5. Send and confirm transaction + // 6. Send and confirm transaction self.send_and_confirm_transaction(&tx).await } /// Check if the wallet has sufficient balance for the trade async fn check_token_balance(&self, mint: &str, amount: u64) -> Result<()> { let token_account = self.find_token_account(mint)?; + + // Check if token account exists + match self.rpc_client.get_account(&token_account) { + Ok(_) => (), + Err(_) => { + println!( + "Token account {} does not exist, creating...", + token_account + ); + self.create_token_account(mint).await?; + } + } + let balance = self.rpc_client.get_token_account_balance(&token_account)?; + println!( + "Current balance of {}: {} (need {})", + mint, + balance.ui_amount.unwrap_or(0.0), + amount as f64 / 10f64.powi(balance.decimals as i32) + ); - if balance.ui_amount.unwrap_or(0.0) * 10f64.powi(balance.decimals as i32) < amount as f64 { + // Compare raw amounts (lamports) instead of UI amounts + if balance.amount.parse::().unwrap_or(0) < amount { return Err(anyhow!( "Insufficient balance: have {} {}, need {}", balance.ui_amount.unwrap_or(0.0), mint, - amount + amount as f64 / 10f64.powi(balance.decimals as i32) )); } Ok(()) } + /// Create token account if it doesn't exist + async fn create_token_account(&self, mint: &str) -> Result<()> { + let mint_pubkey = Pubkey::from_str(mint)?; + let owner = self.wallet.pubkey(); + + let create_ix = spl_associated_token_account::instruction::create_associated_token_account( + &owner, + &owner, + &mint_pubkey, + &spl_token::id(), + ); + + let recent_blockhash = self.rpc_client.get_latest_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[create_ix], + Some(&owner), + &[&self.wallet], + recent_blockhash, + ); + + self.send_and_confirm_transaction(&tx).await?; + println!("Created token account for mint {}", mint); + Ok(()) + } + /// Find the associated token account for a given mint fn find_token_account(&self, mint: &str) -> Result { let mint_pubkey = Pubkey::from_str(mint)?; @@ -153,19 +259,65 @@ impl TradeExecutor { /// Simulate a transaction before sending async fn simulate_transaction(&self, transaction: &Transaction) -> Result<()> { - self.rpc_client - .simulate_transaction(transaction) - .map_err(|e| anyhow!("Transaction simulation failed: {}", e))?; + let sim_result = self.rpc_client.simulate_transaction(transaction)?; + + if let Some(err) = sim_result.value.err { + println!("Simulation error: {:?}", err); + if let Some(logs) = sim_result.value.logs { + println!("Transaction logs:"); + for log in logs { + println!(" {}", log); + } + } + return Err(anyhow!("Transaction simulation failed: {:?}", err)); + } Ok(()) } /// Send and confirm a transaction async fn send_and_confirm_transaction(&self, transaction: &Transaction) -> Result { - let signature = self - .rpc_client - .send_and_confirm_transaction(transaction) - .map_err(|e| anyhow!("Failed to send transaction: {}", e))?; - Ok(signature) + let signature = transaction.signatures[0]; + println!("Sending transaction with signature: {}", signature); + + match self.rpc_client.send_and_confirm_transaction(transaction) { + Ok(_) => { + println!("Transaction confirmed successfully"); + Ok(signature) + } + Err(e) => { + println!("Transaction failed: {}", e); + // Try to get more details about the error + if let Ok(status) = self.rpc_client.get_signature_status(&signature) { + println!("Transaction status: {:?}", status); + } + Err(anyhow!("Failed to send transaction: {}", e)) + } + } + } + + /// Ensure a token account exists for the given mint + pub async fn ensure_token_account(&self, mint: &str) -> Result<()> { + let token_account = self.find_token_account(mint)?; + + // Check if token account exists + match self.rpc_client.get_account(&token_account) { + Ok(_) => { + println!("Token account {} exists", token_account); + Ok(()) + } + Err(_) => { + println!("Creating token account for mint {}", mint); + self.create_token_account(mint).await + } + } + } + + pub async fn get_token_balance(&self, mint: &Pubkey) -> Result { + let token_account = get_associated_token_address(&self.wallet.pubkey(), mint); + match self.rpc_client.get_token_account_balance(&token_account) { + Ok(balance) => Ok(balance.amount.parse().unwrap_or(0)), + Err(_) => Ok(0), // Return 0 if account doesn't exist + } } }