Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 95 additions & 13 deletions packages/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
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_token_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

Expand Down Expand Up @@ -121,13 +119,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
};
Expand Down Expand Up @@ -414,20 +442,42 @@ async fn run_iteration(executor: &Option<TradeExecutor>) -> 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 {
(0, 1)
} 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,
Expand All @@ -441,6 +491,8 @@ async fn run_iteration(executor: &Option<TradeExecutor>) -> 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,
Expand All @@ -454,6 +506,8 @@ async fn run_iteration(executor: &Option<TradeExecutor>) -> 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,
Expand All @@ -466,22 +520,28 @@ async fn run_iteration(executor: &Option<TradeExecutor>) -> 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?;
println!("1. USDC -> CHEESE: {}", sig1);

// 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,
Expand All @@ -495,6 +555,8 @@ async fn run_iteration(executor: &Option<TradeExecutor>) -> 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,
Expand All @@ -507,6 +569,8 @@ async fn run_iteration(executor: &Option<TradeExecutor>) -> 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?;
Expand All @@ -530,6 +594,19 @@ fn find_arbitrage_opportunities(
continue;
}

// Get the other token's mint (non-CHEESE token)
let other_mint = if pool.pool_token_mints[0] == CHEESE_MINT {
&pool.pool_token_mints[1]
} else {
&pool.pool_token_mints[0]
};

// Skip blacklisted tokens
if is_token_blacklisted(other_mint) {
println!("Skipping blacklisted token: {}", other_mint);
continue;
}

let (cheese_ix, other_ix) = if pool.pool_token_mints[0] == CHEESE_MINT {
(0, 1)
} else {
Expand All @@ -538,6 +615,7 @@ fn find_arbitrage_opportunities(

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 fee_percent: f64 = pool.total_fee_pct.trim_end_matches('%').parse::<f64>()? / 100.0;

if cheese_qty <= 0.0 || other_qty <= 0.0 {
Expand All @@ -549,7 +627,11 @@ fn find_arbitrage_opportunities(

// 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
let max_trade_size = if is_usdc_pool {
cheese_qty * 0.1
} else {
cheese_qty * 0.05
}; // 10% of pool liquidity
let price_diff_per_cheese = (implied_price - cheese_usdc_price).abs();
let gross_profit = max_trade_size * price_diff_per_cheese;

Expand Down
1 change: 1 addition & 0 deletions packages/libcheese/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ 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"
22 changes: 22 additions & 0 deletions packages/libcheese/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ use serde::de::{self, Deserializer};
use serde::Deserialize;

pub const CHEESE_MINT: &str = "A3hzGcTxZNSc7744CWB2LR5Tt9VTtEaQYpP6nwripump";
pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
pub const USDC_DECIMALS: u8 = 6;
pub const OSHO_MINT: &str = "27VkFr6b6DHoR6hSYZjUDbwJsV6MPSFqPavXLg8nduHW";
pub const HARA_MINT: &str = "7HW7JWmXKPf5GUgfP1vsXUjPBy7WJtA1YQMLFg62pump";
pub const EMPIRE_MINT: &str = "3G5t554LYng7f4xtKKecHbppvctm8qbkoRiTtpqQEAWy";
pub const BLACKLISTED_TOKENS: &[&str] = &[OSHO_MINT, HARA_MINT, EMPIRE_MINT];

pub fn de_string_to_f64<'de, D>(deserializer: D) -> std::result::Result<f64, D::Error>
where
Expand Down Expand Up @@ -29,3 +35,19 @@ pub fn parse_other_token_name(pool_name: &str) -> String {
}
pool_name.to_string()
}

pub fn get_token_amount_from_ui(ui_amount: f64, decimals: u8) -> u64 {
if decimals == USDC_DECIMALS {
(ui_amount * 10f64.powi(USDC_DECIMALS as i32)) as u64
} else {
(ui_amount * 10f64.powi(decimals as i32)) as u64
}
}

pub fn get_usdc_amount_from_ui(ui_amount: f64) -> u64 {
get_token_amount_from_ui(ui_amount, USDC_DECIMALS)
}

pub fn is_token_blacklisted(mint: &str) -> bool {
BLACKLISTED_TOKENS.contains(&mint)
}
28 changes: 23 additions & 5 deletions packages/libcheese/src/meteora.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ pub async fn get_meteora_quote(
amount_in: u64,
) -> Result<MeteoraQuoteResponse> {
// Get current pool state
println!("Fetching pool state for {}", pool_address);
let pool = fetch_pool_state(client, pool_address).await?;

// Find the indices for input and output tokens
Expand All @@ -111,6 +112,11 @@ pub async fn get_meteora_quote(
(1, 0)
};

println!(
"Pool state: in_idx={}, out_idx={}, amounts={:?}",
in_idx, out_idx, pool.pool_token_amounts
);

// Parse pool amounts
let in_amount_pool: f64 = pool.pool_token_amounts[in_idx].parse()?;
let out_amount_pool: f64 = pool.pool_token_amounts[out_idx].parse()?;
Expand All @@ -129,15 +135,18 @@ pub async fn get_meteora_quote(
let price_after = (out_amount_pool - amount_out) / (in_amount_pool + amount_in as f64);
let price_impact = ((price_before - price_after) / price_before * 100.0).to_string();

Ok(MeteoraQuoteResponse {
let quote = MeteoraQuoteResponse {
pool_address: pool_address.to_string(),
input_mint: input_mint.to_string(),
output_mint: output_mint.to_string(),
in_amount: amount_in.to_string(),
out_amount: amount_out.to_string(),
fee_amount: fee_amount.to_string(),
price_impact,
})
};

println!("Generated quote: {:?}", quote);
Ok(quote)
}

async fn fetch_pool_state(client: &Client, pool_address: &str) -> Result<MeteoraPool> {
Expand Down Expand Up @@ -174,16 +183,25 @@ pub async fn get_meteora_swap_transaction(
quote_response: quote.clone(),
};

// Log the swap request
println!("Sending Meteora swap request: {:?}", swap_request);
println!("Sending swap request to {}: {:?}", swap_url, swap_request);

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?;
println!(
"Received swap transaction (length={})",
swap.transaction.len()
);
Ok(swap.transaction)
}

Expand Down
Loading
Loading