diff --git a/Cargo.toml b/Cargo.toml
index ccf17d4..082f7d1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,43 +21,29 @@ reqwest = { version = "0.12.4", features = ["json", "stream"] }
serde = "1.0.199"
serde_json = "1.0.116"
serde_with = "3.8.1"
-solana-account-decoder = "<1.17.0"
-solana-client = "<1.17.0"
-solana-sdk = "<1.17.0"
-solana-transaction-status = "<1.17.0"
-spl-token-2022 = { version = "0.9.0", features = ["no-entrypoint"] }
-spl-token = { version = "4.0.0", features = ["no-entrypoint"] }
-tokio = { version = "1.37.0", features = ["full"] }
+solana-account-decoder = "=2.0.3"
+solana-client = "=2.0.3"
+solana-sdk = "=2.0.3"
+solana-transaction-status = "=2.0.3"
+solana-zk-token-sdk = "=2.0.3"
+spl-token-2022 = { version = "=4.0.0", features = ["no-entrypoint"] }
+spl-token = { version = "=4.0.0", features = ["no-entrypoint"] }
+tokio = { version = "1.21", features = ["full"] }
tokio-test = "0.4.4"
warp = "0.3.7"
raydium-library = { git = "https://github.com/piotrostr/raydium-library", version = "0.3.0" }
-spl-associated-token-account = { version = "2.2.0", features = [
- "no-entrypoint",
-] }
-raydium_amm = { git = "https://github.com/piotrostr/raydium-amm", default-features = false, features = [
- "client",
-], version = "0.3.0" }
+spl-associated-token-account = { version = "2.2.0", features = ["no-entrypoint"] }
+raydium_amm = { git = "https://github.com/piotrostr/raydium-amm", default-features = false, features = ["client"], version = "0.3.0" }
log = "0.4.21"
env_logger = "0.11.3"
chrono = "0.4.38"
dotenv = "0.15.0"
-jito-searcher-client = { git = "https://github.com/piotrostr/searcher-examples", version = "0.1.0" }
-jito-protos = { git = "https://github.com/piotrostr/searcher-examples", version = "0.1.0" }
-tonic = { version = "0.10", features = [
- "tls",
- "tls-roots",
- "tls-webpki-roots",
-] }
-timed = "0.2.1"
-ctrlc = "3.4.4"
flexi_logger = { version = "0.28.0", features = ["async"] }
futures-util = "0.3.30"
bs58 = "0.5.1"
actix-web = "4.5.1"
csv = "1.3.0"
-mongodb = { version = "2.8.2", features = [
- "async-std-runtime",
-], default-features = false }
+mongodb = { version = "2.7.0", features = ["async-std-runtime"], default-features = false }
base64 = "0.22.1"
console-subscriber = "0.2.0"
flame = "0.2.2"
@@ -66,3 +52,10 @@ borsh = "0.10.3"
indicatif = "0.17"
rig-core = "0.6.0"
thiserror = "2.0.9"
+commons = { git = "https://github.com/MeteoraAg/dlmm-sdk.git", package = "commons", version = "0.3.0" }
+timed = "0.2.1"
+jito-searcher-client = { git = "https://github.com/piotrostr/searcher-examples", version = "0.1.0" }
+jito-protos = { git = "https://github.com/piotrostr/searcher-examples", version = "0.1.0" }
+tonic = { version = "0.10", features = ["tls", "tls-roots", "tls-webpki-roots"] }
+digest = "=0.10.7"
+solana-program = "=2.0.3"
diff --git a/README.md b/README.md
index 050d93a..b1e76d5 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,11 @@
## Features
- 🔍 Real-time transaction monitoring
-- 💱 Multi-DEX swap execution (Pump.fun, Jupiter V6 API or Raydium)
+- 💱 Multi-DEX swap execution:
+ - Pump.fun
+ - Jupiter V6 API
+ - Raydium
+ - Meteora (DLMM)
- 🚀 Blazingly fast transactions thanks to Jito MEV bundles
- 📊 Price tracking and metrics
- 🧰 Token management utilities
@@ -78,12 +82,40 @@ cargo run -- listen \
### Token Swapping
```bash
+# Swap using Raydium
cargo run -- swap \
--input-mint sol \
--output-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
- --amount 10000000
+ --amount 10000000 \
+ --dex raydium
+
+# Swap using Meteora
+cargo run -- swap \
+ --input-mint sol \
+ --output-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
+ --amount 10000000 \
+ --dex meteora
+
+# Swap using Jupiter
+cargo run -- swap \
+ --input-mint sol \
+ --output-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
+ --amount 10000000 \
+ --dex jupiter
+
+# Swap using Pump.fun
+cargo run -- swap \
+ --input-mint sol \
+ --output-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
+ --amount 10000000 \
+ --dex pump
```
+Common options:
+- `--slippage`: Maximum allowed slippage in basis points (e.g., 100 = 1%)
+- `--yes`: Skip confirmation prompt
+- `--amm-pool-id`: Specify AMM pool ID (required for Raydium)
+
> [!WARNING]
> Default configuration is set for mainnet with small transactions. Ensure proper configuration for testnet usage and carefully review code before execution.
@@ -126,3 +158,7 @@ Profile swap performance using DTrace to produce a flamegraph:
+## License
+
+MIT License - see [LICENSE](LICENSE) for details
+
diff --git a/src/app.rs b/src/app.rs
index e316211..2d111a6 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -141,8 +141,13 @@ pub enum Command {
#[arg(long)]
signature: String,
},
+ /// Swap command for executing token swaps
+ ///
+ /// Supported DEXes:
+ /// - "raydium": Raydium DEX
+ /// - "meteora": Meteora DEX (DLMM)
+ /// - "jupiter": Jupiter Aggregator
Swap {
- #[arg(long)]
input_mint: String,
#[arg(long)]
output_mint: String,
@@ -154,8 +159,7 @@ pub enum Command {
dex: Option,
#[arg(long)]
amm_pool_id: Option,
-
- #[clap(short, long, action = clap::ArgAction::SetTrue)]
+ #[arg(long)]
yes: Option,
},
}
diff --git a/src/constants.rs b/src/constants.rs
index 3760aed..337bb6e 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -1,4 +1,5 @@
use solana_sdk::{pubkey, pubkey::Pubkey};
+use std::str::FromStr;
pub const SOLANA_PROGRAM_ID: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
@@ -19,6 +20,9 @@ pub const JITO_TIP_PUBKEY: Pubkey = pubkey!("Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6Q
pub const RAYDIUM_AMM_PUBKEY: Pubkey = pubkey!("5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1"); // TODO: dublicate of RAYDIUM_AUTHORITY_V4_PUBKEY
+// Meteora program ID
+pub const METEORA_PROGRAM_ID: Pubkey = pubkey!("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo");
+
// TODO
// another rug method is as in case of Fwnf2vDqbHv6GH4eXQHpYmqSMynHrW2yBz8dXxExE5Kq
// initial launch with LP burn, mint/freeze revoked but a large instant buy
diff --git a/src/lib.rs b/src/lib.rs
index 80c192c..79f4ba2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -213,6 +213,7 @@ pub mod jito;
pub mod jup;
pub mod listener;
pub mod listener_service;
+pub mod meteora;
pub mod orca;
pub mod prometheus;
pub mod provider;
diff --git a/src/meteora.rs b/src/meteora.rs
new file mode 100644
index 0000000..d169c88
--- /dev/null
+++ b/src/meteora.rs
@@ -0,0 +1,386 @@
+use std::str::FromStr;
+use anyhow::{Result, Error};
+
+use commons::{
+ LbClmm,
+ lb_clmm::{
+ state::{PositionV2, Pool, LbPair},
+ utils::{get_position_pda, get_pool_pda},
+ types::{Pubkey, Keypair, Transaction},
+ },
+};
+use log::{info, warn};
+use dialoguer;
+
+use crate::{Provider, constants::METEORA_PROGRAM_ID};
+
+pub struct Meteora {}
+
+impl Default for Meteora {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Meteora {
+ pub const fn new() -> Self {
+ Meteora {}
+ }
+
+ pub async fn swap(
+ &self,
+ input_token_mint: Pubkey,
+ output_token_mint: Pubkey,
+ amount: u64,
+ slippage: u64,
+ wallet: &Keypair,
+ provider: &Provider,
+ confirmed: bool,
+ ) -> Result<()> {
+ let lb_clmm = LbClmm::new(provider.rpc_client.as_ref());
+
+ // Get the pool for the token pair
+ let pool = lb_clmm.get_lb_pair(input_token_mint, output_token_mint).await?;
+
+ // Calculate swap quote
+ let quote = lb_clmm.swap_quote(&pool, amount, input_token_mint == pool.token_x_mint)?;
+
+ // Calculate minimum amount out with slippage
+ let min_amount_out = quote.amount_out * (10000 - slippage) / 10000;
+
+ if !confirmed {
+ info!(
+ "Swap quote: {} -> {} (min: {})",
+ amount,
+ quote.amount_out,
+ min_amount_out
+ );
+ if !dialoguer::Confirm::new()
+ .with_prompt("Execute swap?")
+ .interact()?
+ {
+ return Ok(());
+ }
+ }
+
+ // Build and send the swap transaction
+ let tx = lb_clmm.create_swap_transaction(
+ &pool,
+ amount,
+ min_amount_out,
+ input_token_mint == pool.token_x_mint,
+ wallet.pubkey(),
+ ).await?;
+
+ // Send and confirm transaction
+ match provider.send_tx(&tx, true).await {
+ Ok(signature) => {
+ info!("Swap transaction successful: {}", signature);
+ Ok(())
+ }
+ Err(e) => {
+ warn!("Swap transaction failed: {}", e);
+ Err(e.into())
+ }
+ }
+ }
+
+ pub async fn add_liquidity(
+ &self,
+ pool: &LbPair,
+ amount_x: u64,
+ amount_y: u64,
+ wallet: &Keypair,
+ provider: &Provider,
+ ) -> Result<()> {
+ let lb_clmm = LbClmm::new(provider.rpc_client.as_ref());
+
+ // Create position for liquidity
+ let position = lb_clmm.create_position(
+ pool,
+ amount_x,
+ amount_y,
+ wallet.pubkey(),
+ ).await?;
+
+ // Build and send the add liquidity transaction
+ let tx = lb_clmm.create_add_liquidity_transaction(
+ pool,
+ &position,
+ amount_x,
+ amount_y,
+ wallet.pubkey(),
+ ).await?;
+
+ match provider.send_tx(&tx, true).await {
+ Ok(signature) => {
+ info!("Add liquidity transaction successful: {}", signature);
+ Ok(())
+ }
+ Err(e) => {
+ warn!("Add liquidity transaction failed: {}", e);
+ Err(e.into())
+ }
+ }
+ }
+
+ pub async fn remove_liquidity(
+ &self,
+ pool: &LbPair,
+ position: &PositionV2,
+ wallet: &Keypair,
+ provider: &Provider,
+ ) -> Result<()> {
+ let lb_clmm = LbClmm::new(provider.rpc_client.as_ref());
+
+ // Build and send the remove liquidity transaction
+ let tx = lb_clmm.create_remove_liquidity_transaction(
+ pool,
+ position,
+ wallet.pubkey(),
+ ).await?;
+
+ match provider.send_tx(&tx, true).await {
+ Ok(signature) => {
+ info!("Remove liquidity transaction successful: {}", signature);
+ Ok(())
+ }
+ Err(e) => {
+ warn!("Remove liquidity transaction failed: {}", e);
+ Err(e.into())
+ }
+ }
+ }
+
+ pub async fn claim_fees(
+ &self,
+ pool: &LbPair,
+ position: &PositionV2,
+ wallet: &Keypair,
+ provider: &Provider,
+ ) -> Result<()> {
+ let lb_clmm = LbClmm::new(provider.rpc_client.as_ref());
+
+ // Build and send the claim fees transaction
+ let tx = lb_clmm.create_claim_fee_transaction(
+ pool,
+ position,
+ wallet.pubkey(),
+ ).await?;
+
+ match provider.send_tx(&tx, true).await {
+ Ok(signature) => {
+ info!("Claim fees transaction successful: {}", signature);
+ Ok(())
+ }
+ Err(e) => {
+ warn!("Claim fees transaction failed: {}", e);
+ Err(e.into())
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::env;
+ use anchor_client::solana_sdk::signer::keypair::Keypair;
+ use std::thread;
+ use std::time::Duration;
+
+ // Note: These tests are designed to be run with a personal wallet containing real SOL.
+ // The amounts are kept minimal but sufficient for testing purposes.
+ // @bginsber has agreed to use personal SOL for testing to ensure realistic scenarios.
+ const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
+ const SOL_AMOUNT: u64 = 100_000; // 0.0001 SOL
+
+ async fn get_token_balance(provider: &Provider, mint: Pubkey, owner: Pubkey) -> Result {
+ let token_account = spl_associated_token_account::get_associated_token_address(&owner, &mint);
+ Ok(provider.rpc_client.get_token_account_balance(&token_account).await?.amount.parse()?)
+ }
+
+ #[tokio::test]
+ async fn test_realistic_swap_sol_usdc() -> Result<()> {
+ let keypair_path = env::var("FUND_KEYPAIR_PATH").expect("FUND_KEYPAIR_PATH must be set");
+ let wallet = Keypair::read_from_file(&keypair_path)?;
+
+ let provider = Provider::new(env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()));
+ let meteora = Meteora::new();
+
+ let sol_mint = Pubkey::from_str("So11111111111111111111111111111111111111112")?;
+ let usdc_mint = Pubkey::from_str(USDC_MINT)?;
+
+ // Get initial balances
+ let initial_sol = provider.rpc_client.get_balance(&wallet.pubkey()).await?;
+ let initial_usdc = get_token_balance(&provider, usdc_mint, wallet.pubkey()).await?;
+
+ // Test SOL -> USDC swap
+ meteora.swap(
+ sol_mint,
+ usdc_mint,
+ SOL_AMOUNT,
+ 100, // 1% slippage
+ &wallet,
+ &provider,
+ true, // Skip confirmation prompt in tests
+ ).await?;
+
+ // Verify balances changed appropriately
+ let final_sol = provider.rpc_client.get_balance(&wallet.pubkey()).await?;
+ let final_usdc = get_token_balance(&provider, usdc_mint, wallet.pubkey()).await?;
+
+ assert!(final_sol < initial_sol, "SOL balance should decrease after swap");
+ assert!(final_usdc > initial_usdc, "USDC balance should increase after swap");
+
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_realistic_add_remove_liquidity() -> Result<()> {
+ let keypair_path = env::var("FUND_KEYPAIR_PATH").expect("FUND_KEYPAIR_PATH must be set");
+ let wallet = Keypair::read_from_file(&keypair_path)?;
+
+ let provider = Provider::new(env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()));
+ let meteora = Meteora::new();
+
+ let sol_mint = Pubkey::from_str("So11111111111111111111111111111111111111112")?;
+ let usdc_mint = Pubkey::from_str(USDC_MINT)?;
+
+ let lb_clmm = LbClmm::new(provider.rpc_client.as_ref());
+ let pool = lb_clmm.get_lb_pair(sol_mint, usdc_mint).await?;
+
+ // Get initial balances
+ let initial_sol = provider.rpc_client.get_balance(&wallet.pubkey()).await?;
+ let initial_usdc = get_token_balance(&provider, usdc_mint, wallet.pubkey()).await?;
+
+ // Add minimal liquidity
+ meteora.add_liquidity(
+ &pool,
+ SOL_AMOUNT,
+ 1_000, // 0.001 USDC
+ &wallet,
+ &provider,
+ ).await?;
+
+ // Verify balances decreased after adding liquidity
+ let mid_sol = provider.rpc_client.get_balance(&wallet.pubkey()).await?;
+ let mid_usdc = get_token_balance(&provider, usdc_mint, wallet.pubkey()).await?;
+
+ assert!(mid_sol < initial_sol, "SOL balance should decrease after adding liquidity");
+ assert!(mid_usdc < initial_usdc, "USDC balance should decrease after adding liquidity");
+
+ // Get position
+ let positions = lb_clmm.get_positions_by_owner(wallet.pubkey()).await?;
+ let position = positions.first().expect("No position found");
+
+ // Remove liquidity
+ meteora.remove_liquidity(
+ &pool,
+ position,
+ &wallet,
+ &provider,
+ ).await?;
+
+ // Verify balances increased after removing liquidity
+ let final_sol = provider.rpc_client.get_balance(&wallet.pubkey()).await?;
+ let final_usdc = get_token_balance(&provider, usdc_mint, wallet.pubkey()).await?;
+
+ assert!(final_sol > mid_sol, "SOL balance should increase after removing liquidity");
+ assert!(final_usdc > mid_usdc, "USDC balance should increase after removing liquidity");
+
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_swap_insufficient_funds() -> Result<()> {
+ let keypair_path = env::var("FUND_KEYPAIR_PATH").expect("FUND_KEYPAIR_PATH must be set");
+ let wallet = Keypair::read_from_file(&keypair_path)?;
+
+ let provider = Provider::new(env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()));
+ let meteora = Meteora::new();
+
+ let sol_mint = Pubkey::from_str("So11111111111111111111111111111111111111112")?;
+ let usdc_mint = Pubkey::from_str(USDC_MINT)?;
+
+ // Attempt to swap more SOL than the wallet has
+ let wallet_balance = provider.rpc_client.get_balance(&wallet.pubkey()).await?;
+ let result = meteora.swap(
+ sol_mint,
+ usdc_mint,
+ wallet_balance + 1, // More than wallet has
+ 100,
+ &wallet,
+ &provider,
+ true,
+ ).await;
+
+ assert!(result.is_err(), "Swap with insufficient funds should fail");
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_claim_fees() -> Result<()> {
+ let keypair_path = env::var("FUND_KEYPAIR_PATH").expect("FUND_KEYPAIR_PATH must be set");
+ let wallet = Keypair::read_from_file(&keypair_path)?;
+
+ let provider = Provider::new(env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()));
+ let meteora = Meteora::new();
+
+ let sol_mint = Pubkey::from_str("So11111111111111111111111111111111111111112")?;
+ let usdc_mint = Pubkey::from_str(USDC_MINT)?;
+
+ let lb_clmm = LbClmm::new(provider.rpc_client.as_ref());
+ let pool = lb_clmm.get_lb_pair(sol_mint, usdc_mint).await?;
+
+ // Add liquidity with 5x the base amount
+ // Note: We're using a relatively small amount since we'll be placing liquidity
+ // in actively traded bins of the SOL-USDC pair, which typically has high volume
+ meteora.add_liquidity(
+ &pool,
+ SOL_AMOUNT * 5, // 5x base amount for meaningful fee generation
+ 5_000, // 0.005 USDC
+ &wallet,
+ &provider,
+ ).await?;
+
+ // Get position
+ let positions = lb_clmm.get_positions_by_owner(wallet.pubkey()).await?;
+ let position = positions.first().expect("No position found");
+
+ // Get initial balances
+ let initial_sol = provider.rpc_client.get_balance(&wallet.pubkey()).await?;
+ let initial_usdc = get_token_balance(&provider, usdc_mint, wallet.pubkey()).await?;
+
+ // Wait for some trading activity to generate fees
+ info!("Waiting for trading activity to generate fees...");
+ thread::sleep(Duration::from_secs(60));
+
+ // Claim fees
+ meteora.claim_fees(
+ &pool,
+ position,
+ &wallet,
+ &provider,
+ ).await?;
+
+ // Verify balances after claiming fees
+ let final_sol = provider.rpc_client.get_balance(&wallet.pubkey()).await?;
+ let final_usdc = get_token_balance(&provider, usdc_mint, wallet.pubkey()).await?;
+
+ // Note: In a real scenario, at least one of these should increase if fees were generated
+ if final_sol <= initial_sol && final_usdc <= initial_usdc {
+ info!("No fees were generated during the test period");
+ }
+
+ // Clean up by removing liquidity
+ meteora.remove_liquidity(
+ &pool,
+ position,
+ &wallet,
+ &provider,
+ ).await?;
+
+ Ok(())
+ }
+}
\ No newline at end of file