Repository: wisarmy/raytx Branch: main Commit: b6a9f4d84f86 Files: 27 Total size: 93.0 KB Directory structure: gitextract_labelvwj/ ├── .env.example ├── .github/ │ └── workflows/ │ └── release.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── docs/ │ ├── api.md │ └── jito.md ├── examples/ │ ├── jito_tip_accounts.rs │ ├── jito_tip_stream.rs │ ├── pool_info.rs │ ├── pump.rs │ └── rpc.rs └── src/ ├── api.rs ├── constants.rs ├── helper.rs ├── jito/ │ ├── api.rs │ ├── mod.rs │ └── ws.rs ├── lib.rs ├── logger.rs ├── main.rs ├── pool.rs ├── pump.rs ├── raydium.rs ├── swap.rs ├── token.rs └── tx.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .env.example ================================================ # Wallet PRIVATE_KEY= # Connection: Comma-separated list RPC_ENDPOINTS=https://api.mainnet-beta.solana.com,https://api.mainnet-beta.solana.com RPC_WEBSOCKET_ENDPOINTS=wss://api.mainnet-beta.solana.com COMMITMENT_LEVEL=confirmed # swap settings #HTTP_PROXY=http://127.0.0.1:1087 SLIPPAGE=10 # priority fees settings # max priority fees = UNIT_PRICE * UNIT_LIMIT (micro-lamports) UNIT_PRICE=20000 # micro-lamports, 1 lamport = 1,000,000 micro-lamports (10^6) UNIT_LIMIT=200000 # jito (Recommend) JITO_BLOCK_ENGINE_URL=https://mainnet.block-engine.jito.wtf # https://docs.jito.wtf/lowlatencytxnsend/#websocket-showing-tip-amounts JITO_TIP_STREAM_URL=wss://bundles.jito.wtf/api/v1/bundles/tip_stream # only support: 25 50 75 95 99 # ref https://jito-labs.metabaseapp.com/public/dashboard/016d4d60-e168-4a8f-93c7-4cd5ec6c7c8d JITO_TIP_PERCENTILE=50 # JITO_TIP_VALUE= # float64, if set, JITO_TIP_PERCENTILE will be ignored # open simulate mode to see what went wrong TX_SIMULATE=false ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - "v*" jobs: release: name: Release - ${{ matrix.platform.release_for }} permissions: write-all runs-on: ${{ matrix.platform.os }} strategy: matrix: platform: - release_for: Linux-x86_64 os: ubuntu-latest target: x86_64-unknown-linux-gnu bin: raytx name: raytx-linux-amd64 - release_for: Windows-x86_64 os: windows-latest target: x86_64-pc-windows-msvc bin: raytx.exe name: raytx-windows-amd64.exe - release_for: macOS-x86_64 os: macos-latest target: x86_64-apple-darwin bin: raytx name: raytx-macos-amd64 - release_for: macOS-aarch64 os: macos-latest target: aarch64-apple-darwin bin: raytx name: raytx-macos-arm64 steps: - uses: actions/checkout@v4 - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.platform.target }} - name: Build run: | cargo build --release --target ${{ matrix.platform.target }} - name: Prepare assets shell: bash run: | cd target/${{ matrix.platform.target }}/release mv ${{ matrix.platform.bin }} ${{ matrix.platform.name }} - name: Upload binaries to release uses: softprops/action-gh-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: files: | target/${{ matrix.platform.target }}/release/${{ matrix.platform.name }} draft: false prerelease: false ================================================ FILE: .gitignore ================================================ /target .env* !.env.example .local ================================================ FILE: Cargo.toml ================================================ [package] name = "raytx" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0.53" dotenvy = "0.15.7" clap = { version = "4.5.7", features = ["derive"] } reqwest = { version = "0.11.27", features = ["json", "socks", "native-tls"] } tokio = { version = "1.38.0", features = ["full"] } serde = "1.0.203" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } serde_json = "1.0.117" rust_decimal = "1.35.0" spl-token = { version = "4.0.0", features = ["no-entrypoint"] } solana-client = "=1.16.27" solana-sdk = "=1.16.27" solana-account-decoder = "=1.16.27" spl-token-client = "=0.7.1" amm-cli = { git = "https://github.com/raydium-io/raydium-library" } common = { git = "https://github.com/raydium-io/raydium-library" } raydium_amm = { git = "https://github.com/raydium-io/raydium-amm", default-features = false, features = [ "client", ] } spl-token-2022 = { version = "0.9.0", features = ["no-entrypoint"] } spl-associated-token-account = { version = "2.2.0", features = [ "no-entrypoint", ] } tokio-tungstenite = { version = "0.23.1", features = ["native-tls"] } futures-util = "0.3.30" jito-json-rpc-client = { git = "https://github.com/wisarmy/jito-block-engine-json-rpc-client.git", package = "jito-block-engine-json-rpc-client" } rand = "0.8.5" indicatif = "0.17.8" axum = { version = "0.7.5", features = ["macros"] } tower-http = { version = "0.5.2", features = ["cors"] } borsh = { version = "1.5.3" } borsh-derive = "1.5.3" [dev-dependencies] ctor = "0.2.8" [features] slow_tests = [] used_linker = [] ================================================ FILE: README.md ================================================ # Raytx Raytx is a powerful tool for performing token swap operations on Raydium and Pump.fun, providing both CLI and API interfaces. ## Features - Command-line interface for quick swaps - RESTful API service for programmatic access - Support for buy/sell operations - Integration with Jito for faster transactions - Percentage-based selling options ## Project Dependencies Before getting started, ensure that the following software is installed on your system: - [Rust](https://www.rust-lang.org/) version 1.8 or higher. ## Build ``` cargo build -r ``` This will generate an executable file raytx, located in the `target/release/raytx`. ## Using the Command-Line Tool ### Buy ``` raytx swap buy --amount-in= ``` ### Sell ``` # sell 50% raytx swap sell --amount-in-pct=0.5 # sell all, close wallet ata when sell all raytx swap sell --amount-in-pct=1 # Sell 1000 raytx swap sell --amount-in=1000 ``` Replace with the address of the token you want to swap, and with the quantity| with the percentage you want to swap. ### Jito Use `--jito` to speed up swap. [Read more](./docs/jito.md) ## Using swap api To start the daemon, use the following command: ```bash raytx daemon ``` [More information in the documentation](./docs/api.md) # Contributing Contributions to this project are welcome. If you have any questions or suggestions, feel free to raise an issue. # License This project is licensed under the MIT License. ================================================ FILE: docs/api.md ================================================ # Buy/Sell ``` curl -X POST http://127.0.0.1:7235/api/swap \ -H "Content-Type: application/json" \ -d '{ "mint": "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", "direction": "buy|sell", "amount_in": 0.001, "slippage": 20, "jito": false|true }' ``` # Sell Proportionally Set `in_type` to `pct` `amount_in` is the percentage; when `amount_in=1`, it will sell all and close ATA ``` curl -X POST http://127.0.0.1:7235/api/swap \ -H "Content-Type: application/json" \ -d '{ "mint": "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", "direction": "sell", "amount_in": 1, "in_type": "pct", "slippage": 20, "jito": false|true }' ``` # Get pool price ``` curl http://127.0.0.1:7235/api/pool/{pool_id} ``` Response: ```json { "data": { "base": 152897118.502952, "price": 0.000103805, "quote": 110.340824464, "sol_price": 143.84 }, "status": "ok" } ``` # Get coin ``` http://127.0.0.1:7235/api/coins/{mint} ``` ```json { "data": { "associated_bonding_curve": "E82g93v8gHWULYFfhmFushJZFEG4fP7PiBgNQefioCqj", "bonding_curve": "4PobGYLLEs8niNg1bWNreNZgu8pDPwYH5ytgmCoxKpfC", "complete": true, "created_timestamp": 1732591590787, "creator": "FzfTq6vGy8vvns5J6xbnh3WeTRWHm6MwATWrYBKyRyar", "description": "", "image_uri": "https://ipfs.io/ipfs/Qmayxq68yjipGKUWMPriCXVCENFqhd8P3tyszAyAnnLuVr", "inverted": true, "is_currently_live": false, "king_of_the_hill_timestamp": 1732591699000, "last_reply": 1732593476763, "market_cap": 47.72, "market_id": "7H6Ybc7LYTzTE6MK7Ai7h9utfqArvAoMpDHBH1CueGaK", "metadata_uri": "https://ipfs.io/ipfs/QmP72w77xYPzoGNvYvietLVKKpjYX12uFFnpLmhdwaztfC", "mint": "EQitNE2QozWdyaz11eq2nVtrLqLUgwKLyXxhBwtZpump", "name": "Justice for Stephen Singleton", "nsfw": false, "profile_image": null, "raydium_info": { "base": 604542889.853835, "price": 4.78322920049418e-8, "quote": 28.916672037 }, "raydium_pool": "9XBq7pkEmhP7E7qEqEoko3hvadrNjiLJRfXS3NJdyLK8", "reply_count": 301, "show_name": true, "symbol": "Stephen", "telegram": null, "total_supply": 1000000000000000, "twitter": "https://x.com/marionawfal/status/1861249022159122444?s=46&t=f-10UPDsIV3KvlJrv0_W6A", "usd_market_cap": 11361.1776, "username": "meowster1", "virtual_sol_reserves": 115005359175, "virtual_token_reserves": 279900000000000, "website": null }, "status": "ok" } ``` # Get token accounts ``` curl http://127.0.0.1:7235/api/token-accounts ``` Response: ```json { "data": [ { "amount": "0", "mint": "Fof1DyVSYiQGCnT3uTbmq8kQMPdwL35x1bD82NaTs9mM", "pubkey": "H3rveEcUaRwNEyaHgmo5F8Jnz1pqP7c1U8ePPHhyjdqV", "ui_amount": 0 }, { "amount": "0", "mint": "7ijK2wWEPSUHgMRpVawWQiAiMuNnEuvV5GbEyBrTpump", "pubkey": "F8qyryJjXESXcoEnw5TnVWpEpkQpvGz47oq41Mn8fuLE", "ui_amount": 0 } ], "status": "ok" } ``` # Get token account ``` curl http://127.0.0.1:7235/api/token-accounts/Fof1DyVSYiQGCnT3uTbmq8kQMPdwL35x1bD82NaTs9mM ``` Response: ```json { "data": { "amount": "0", "mint": "Fof1DyVSYiQGCnT3uTbmq8kQMPdwL35x1bD82NaTs9mM", "pubkey": "H3rveEcUaRwNEyaHgmo5F8Jnz1pqP7c1U8ePPHhyjdqV", "ui_amount": 0 }, "status": "ok" } ``` ================================================ FILE: docs/jito.md ================================================ # Introduction [jito docs](https://docs.jito.wtf/) # Get tip accounts ``` curl https://mainnet.block-engine.jito.wtf/api/v1/bundles -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTipAccounts", "params": [] } ' # response { "jsonrpc": "2.0", "result": [ "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY" ], "id": 1 } ``` # Mainnet ## Tip Payment Program: T1pyyaTNZsKv2WcRAB8oVnk93mLJw2XzjtVYqCsaHqt ## Tip Distribution Program: 4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7 ## WebSocket showing tip amounts: ws://bundles-api-rest.jito.wtf/api/v1/bundles/tip_stream ## Tip dashoard https://jito-labs.metabaseapp.com/public/dashboard/016d4d60-e168-4a8f-93c7-4cd5ec6c7c8d # Mainnet Addresses ## Amsterdam BLOCK_ENGINE_URL=https://amsterdam.mainnet.block-engine.jito.wtf SHRED_RECEIVER_ADDR=74.118.140.240:1002 RELAYER_URL=http://amsterdam.mainnet.relayer.jito.wtf:8100 ## Frankfurt BLOCK_ENGINE_URL=https://frankfurt.mainnet.block-engine.jito.wtf SHRED_RECEIVER_ADDR=145.40.93.84:1002 RELAYER_URL=http://frankfurt.mainnet.relayer.jito.wtf:8100 ## New York BLOCK_ENGINE_URL=https://ny.mainnet.block-engine.jito.wtf SHRED_RECEIVER_ADDR=141.98.216.96:1002 RELAYER_URL=http://ny.mainnet.relayer.jito.wtf:8100 ## Tokyo BLOCK_ENGINE_URL=https://tokyo.mainnet.block-engine.jito.wtf SHRED_RECEIVER_ADDR=202.8.9.160:1002 RELAYER_URL=http://tokyo.mainnet.relayer.jito.wtf:8100 ## Salt Lake City BLOCK_ENGINE_URL=https://slc.mainnet.block-engine.jito.wtf SHRED_RECEIVER_ADDR=64.130.53.8:1002 RELAYER_URL=http://slc.mainnet.relayer.jito.wtf:8100 ================================================ FILE: examples/jito_tip_accounts.rs ================================================ use anyhow::Result; use raytx::jito::api::{get_tip_accounts, TipAccountResult}; #[tokio::main] async fn main() -> Result<()> { let accounts: TipAccountResult = get_tip_accounts().await?.try_into()?; println!("tip accounts: {:#?}", accounts); Ok(()) } ================================================ FILE: examples/jito_tip_stream.rs ================================================ use anyhow::Result; use raytx::jito::{ws::tip_stream, TIPS_PERCENTILE}; #[tokio::main] async fn main() -> Result<()> { tokio::spawn(async { if let Err(e) = tip_stream().await { println!("Error: {:?}", e); } }); loop { { let state = TIPS_PERCENTILE.read().await; if let Some(ref msg) = *state { println!("Latest message: {:?}", msg); } else { println!("No message received yet"); } } println!("Waiting next after 5s"); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; } } ================================================ FILE: examples/pool_info.rs ================================================ use anyhow::Result; use raytx::raydium::get_pool_info; #[tokio::main] async fn main() -> Result<()> { let pool_info = get_pool_info( "So11111111111111111111111111111111111111112", "6FVyLVhQsShWVUsCq2FJRr1MrECGShc3QxBwWtgiVFwK", ) .await?; println!("pool info: {:#?}", pool_info); Ok(()) } ================================================ FILE: examples/pump.rs ================================================ use std::str::FromStr; use anyhow::Result; use raytx::{ get_rpc_client, pump::{get_bonding_curve_account, get_pda, PUMP_PROGRAM}, }; use solana_sdk::pubkey::Pubkey; #[tokio::main] async fn main() -> Result<()> { dotenvy::dotenv().ok(); // let pump_info = get_pump_info("8zSLdDzM1XsqnfrHmHvA9ir6pvYDjs8UXz6B2Tydd6b2").await?; // println!("pump info: {:#?}", pump_info); get_bonding_curve_by_mint().await?; Ok(()) } pub async fn get_bonding_curve_by_mint() -> Result<()> { let client = get_rpc_client()?; let program_id = Pubkey::from_str(PUMP_PROGRAM)?; let mint = Pubkey::from_str("8oAK7mKMSnsVgrBgFS6A4uPqL8dh5NHAc7ohsq71pump")?; let bonding_curve = get_pda(&mint, &program_id)?; println!("bonding_curve: {bonding_curve}"); let bonding_curve_account = get_bonding_curve_account(client, &mint, &Pubkey::from_str(PUMP_PROGRAM)?).await; println!("bonding_curve_account: {:#?}", bonding_curve_account); Ok(()) } ================================================ FILE: examples/rpc.rs ================================================ use std::{env, str::FromStr}; use amm_cli::load_amm_keys; use anyhow::{Context, Result}; use common::common_utils; use futures_util::{SinkExt, StreamExt}; use raytx::{get_rpc_client, logger, pump::PUMP_PROGRAM, raydium::get_pool_state_by_mint}; use solana_client::rpc_client::GetConfirmedSignaturesForAddress2Config; use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey}; use tokio_tungstenite::{connect_async, tungstenite::Message}; use tracing::{error, info}; #[tokio::main] async fn main() -> Result<()> { dotenvy::dotenv().ok(); logger::init(); // get_signatures().await?; // connect_websocket().await?; // get_amm_info().await?; get_amm_info_by_mint().await?; Ok(()) } pub async fn get_amm_info_by_mint() -> Result<()> { let client = get_rpc_client()?; let mint = "DrEMQaQqGN2fQwiUgJi6NStLtmni8m3uSkUP678Apump"; let pool_state = get_pool_state_by_mint(client, mint).await?; println!("pool_state: {:#?}", pool_state); Ok(()) } pub async fn get_amm_info() -> Result<()> { let client = get_rpc_client()?; // let amm_pool_id = Pubkey::from_str("3vehHGc8J9doSo6gJoWYG23JG54hc2i7wjdFReX3Rcah")?; let amm_pool_id = Pubkey::from_str("7Sp76Pv48RaL4he2BfGUhvjqCtvjjfTSnXDXNvk845yL")?; let pool_state = common::rpc::get_account::(&client, &amm_pool_id)?.unwrap(); println!("pool_state : {:#?}", pool_state); let amm_program = Pubkey::from_str("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8")?; let native_mint = spl_token::native_mint::ID; let amount_specified = 100_000_000; let slippage_bps = 10; let swap_base_in = true; let user_input_token = if pool_state.coin_vault_mint == native_mint { pool_state.pc_vault } else { pool_state.coin_vault }; let amm_keys = load_amm_keys(&client, &amm_program, &amm_pool_id).unwrap(); // reload accounts data to calculate amm pool vault amount // get multiple accounts at the same time to ensure data consistency let load_pubkeys = vec![ amm_pool_id, amm_keys.amm_pc_vault, amm_keys.amm_coin_vault, user_input_token, ]; let rsps = common::rpc::get_multiple_accounts(&client, &load_pubkeys).unwrap(); println!("rsps: {:#?}", rsps); let amm_pc_vault_account = rsps[1].clone(); let amm_coin_vault_account = rsps[2].clone(); let _token_in_account = rsps[3].clone(); let amm_pc_vault = common_utils::unpack_token(&amm_pc_vault_account.as_ref().unwrap().data).unwrap(); let amm_coin_vault = common_utils::unpack_token(&amm_coin_vault_account.as_ref().unwrap().data).unwrap(); println!("amm_pc_vault: {:#?}", amm_pc_vault.base.amount); println!("amm_coin_vault: {:#?}", amm_coin_vault.base.amount); let swap_info_result = amm_cli::calculate_swap_info( &client, amm_program, amm_pool_id, user_input_token, amount_specified, slippage_bps, swap_base_in, ) .unwrap(); println!("swap_info_result : {:#?}", swap_info_result); Ok(()) } pub async fn get_signatures() -> Result<()> { let client = get_rpc_client()?; let config = GetConfirmedSignaturesForAddress2Config { before: None, until: None, limit: Some(3), commitment: Some(CommitmentConfig::confirmed()), }; let address = Pubkey::from_str(PUMP_PROGRAM)?; let signatures = client.get_signatures_for_address_with_config(&address, config)?; for signature in signatures { info!("{:#?}", signature); } Ok(()) } pub async fn connect_websocket() -> Result<()> { let (ws_stream, _) = connect_async(env::var("RPC_WEBSOCKET_ENDPOINT")?) .await .context("Failed to connect to WebSocket server")?; info!("Connected to WebSocket server: sol websocket"); let (mut write, mut read) = ws_stream.split(); let _program_subscribe = serde_json::json!({ "jsonrpc": "2.0", "id": 1, "method": "programSubscribe", "params": [ PUMP_PROGRAM, { "encoding": "jsonParsed", "commitment": "processed" } ] }); let logs_subscribe = serde_json::json!({ "jsonrpc": "2.0", "id": 1, "method": "logsSubscribe", "params": [ { "mentions": [ PUMP_PROGRAM ] }, { "commitment": "processed" } ] }); tokio::spawn(async move { let msg = Message::text(logs_subscribe.to_string()); write.send(msg).await.expect("Failed to send message"); }); while let Some(message) = read.next().await { match message { Ok(Message::Text(text)) => { let response: serde_json::Value = serde_json::from_str(&text).unwrap(); info!("Received text message: {:#?}", response); } Ok(Message::Close(close)) => { info!("Connection closed: {:?}", close); break; } Err(e) => { error!("Error receiving message: {:?}", e); break; } _ => { info!("unkown message"); } } } Ok(()) } ================================================ FILE: src/api.rs ================================================ use std::{env, str::FromStr, sync::Arc}; use axum::{ debug_handler, extract::{Path, State}, response::IntoResponse, Json, }; use serde::Deserialize; use serde_json::json; use solana_client::rpc_client::RpcClient; use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; use tracing::{info, warn}; use crate::{ get_rpc_client, helper::{api_error, api_ok}, pump::{get_pump_info, RaydiumInfo}, raydium::Raydium, swap::{self, SwapDirection, SwapInType}, token, }; #[derive(Clone)] pub struct AppState { pub client: Arc, pub wallet: Arc, } #[derive(Debug, Deserialize)] pub struct CreateSwap { mint: String, direction: SwapDirection, amount_in: f64, in_type: Option, slippage: Option, jito: Option, } #[debug_handler] pub async fn swap( State(state): State, Json(input): Json, ) -> impl IntoResponse { let slippage = match input.slippage { Some(v) => v, None => { let slippage = env::var("SLIPPAGE").unwrap_or("5".to_string()); let slippage = slippage.parse::().unwrap_or(5); slippage } }; info!("{:?}, slippage: {}", input, slippage); let result = swap::swap( state, input.mint.as_str(), input.amount_in, input.direction.clone(), input.in_type.unwrap_or(SwapInType::Qty), slippage, input.jito.unwrap_or(false), ) .await; match result { Ok(txs) => api_ok(txs), Err(err) => { warn!("swap err: {:#?}", err); api_error(&err.to_string()) } } } #[debug_handler] pub async fn get_pool( State(state): State, Path(pool_id): Path, ) -> impl IntoResponse { let client = match get_rpc_client() { Ok(client) => client, Err(err) => { return api_error(&format!("failed to get rpc client: {err}")); } }; let wallet = state.wallet; let swapx = Raydium::new(client, wallet); match swapx.get_pool(pool_id.as_str()).await { Ok(data) => api_ok(json!({ "base": data.0, "quote": data.1, "price": data.2, "usd_price": data.3, "sol_price": data.4, })), Err(err) => { warn!("get pool err: {:#?}", err); api_error(&err.to_string()) } } } pub async fn coins(State(state): State, Path(mint): Path) -> impl IntoResponse { let client = match get_rpc_client() { Ok(client) => client, Err(err) => { return api_error(&format!("failed to get rpc client: {err}")); } }; let wallet = state.wallet; // query from pump.fun let mut pump_info = match get_pump_info(client.clone(), &mint).await { Ok(info) => info, Err(err) => { return api_error(&err.to_string()); } }; if pump_info.complete { let swapx = Raydium::new(client, wallet); match swapx.get_pool_price(None, Some(mint.as_str())).await { Ok(data) => { pump_info.raydium_info = Some(RaydiumInfo { base: data.0, quote: data.1, price: data.2, }); } Err(err) => { warn!("get raydium pool price err: {:#?}", err); } } } return api_ok(pump_info); } #[debug_handler] pub async fn token_accounts(State(state): State) -> impl IntoResponse { let client = match get_rpc_client() { Ok(client) => client, Err(err) => { return api_error(&format!("failed to get rpc client: {err}")); } }; let wallet = state.wallet; let token_accounts = token::token_accounts(&client, &wallet.pubkey()).await; match token_accounts { Ok(token_accounts) => api_ok(token_accounts), Err(err) => { warn!("get token_accounts err: {:#?}", err); api_error(&err.to_string()) } } } #[debug_handler] pub async fn token_account( State(state): State, Path(mint): Path, ) -> impl IntoResponse { let client = match get_rpc_client() { Ok(client) => client, Err(err) => { return api_error(&format!("failed to get rpc client: {err}")); } }; let wallet = state.wallet; let mint = if let Ok(mint) = Pubkey::from_str(mint.as_str()) { mint } else { return api_error("invalid mint pubkey"); }; let token_account = token::token_account(&client, &wallet.pubkey(), mint).await; match token_account { Ok(token_account) => api_ok(token_account), Err(err) => { warn!("get token_account err: {:#?}", err); api_error(&err.to_string()) } } } ================================================ FILE: src/constants.rs ================================================ pub struct Symbol; impl Symbol { pub const SOLANA: &'static str = "solana"; } ================================================ FILE: src/helper.rs ================================================ use std::collections::HashMap; use anyhow::{anyhow, Context, Result}; use axum::Json; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use crate::get_client_build; pub fn api_ok(data: T) -> Json { Json(json!({ "status": "ok", "data": data })) } pub fn api_error(msg: &str) -> Json { Json(json!({ "status": "error", "message": msg })) } #[derive(Debug, Deserialize)] struct CurrencyData { usd: f64, } // get sol price from coingecko // https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd pub async fn get_price(name: &str) -> Result { let client = get_client_build()?; let result = client .get("https://api.coingecko.com/api/v3/simple/price") .query(&[("ids", name), ("vs_currencies", "usd")]) .send() .await? .json::>() .await .context("Failed to parse price JSON")?; Ok(result .get(name) .ok_or(anyhow!("failed get {} currency data", name))? .usd) } // get sol price from pump.fun // https://frontend-api.pump.fun/sol-price pub async fn get_solana_price() -> Result { let client = get_client_build()?; let result = client .get("https://frontend-api.pump.fun/sol-price") .send() .await? .json::>() .await .context("Failed to parse price JSON")?; let sol_price = result .get("solPrice") .ok_or(anyhow!("failed get sol price"))?; Ok(*sol_price) } #[cfg(test)] mod tests { use tracing::debug; use super::*; #[tokio::test] async fn test_get_solana_price() { let price = get_solana_price().await.unwrap(); debug!("sol price: {}", price); assert!(price > 0.0) } } ================================================ FILE: src/jito/api.rs ================================================ use std::env; use anyhow::{Context, Result}; use reqwest::Proxy; use serde::{Deserialize, Serialize}; use super::{TipPercentileData, BLOCK_ENGINE_URL}; #[derive(Serialize)] struct RpcRequest { jsonrpc: String, id: u32, method: String, params: Vec<()>, } #[derive(Deserialize, Debug)] pub struct RpcResponse { pub jsonrpc: String, pub id: Option, pub result: Option, pub error: Option, } pub async fn get_tip_accounts() -> Result { let mut client_builder = reqwest::Client::builder(); if let Ok(http_proxy) = env::var("HTTP_PROXY") { let proxy = Proxy::all(http_proxy)?; client_builder = client_builder.proxy(proxy); } let client = client_builder.build()?; let request_body = RpcRequest { jsonrpc: "2.0".to_string(), id: 1, method: "getTipAccounts".to_string(), params: vec![], }; let result = client .post(format!("{}/api/v1/bundles", *BLOCK_ENGINE_URL)) .json(&request_body) .send() .await? .json::() .await?; Ok(result) } /// tip accounts #[derive(Debug)] pub struct TipAccountResult { pub accounts: Vec, } impl TryFrom for TipAccountResult { type Error = anyhow::Error; fn try_from(value: RpcResponse) -> Result { if let Some(error) = value.error { return Err(anyhow::anyhow!("RPC error: {}", error)); } let result = value.result.context("missing 'result' field in response")?; let accounts = result .as_array() .context("expected 'result' to be an array")? .iter() .map(|v| v.as_str().unwrap().to_string()) .collect(); Ok(TipAccountResult { accounts }) } } pub async fn get_tip_amounts() -> Result> { let mut client_builder = reqwest::Client::builder(); if let Ok(http_proxy) = env::var("HTTP_PROXY") { let proxy = Proxy::all(http_proxy)?; client_builder = client_builder.proxy(proxy); } let client = client_builder.build()?; let result = client .get("https://bundles.jito.wtf/api/v1/bundles/tip_floor") .send() .await? .json::>() .await?; Ok(result) } ================================================ FILE: src/jito/mod.rs ================================================ use std::{future::Future, str::FromStr, sync::LazyLock, time::Duration}; use anyhow::{anyhow, Result}; use api::{get_tip_accounts, TipAccountResult}; use indicatif::{ProgressBar, ProgressStyle}; use rand::{seq::IteratorRandom, thread_rng}; use serde::Deserialize; use serde_json::Value; use solana_sdk::pubkey::Pubkey; use tokio::{ sync::RwLock, time::{sleep, Instant}, }; use tracing::{debug, error, info, warn}; use crate::get_env_var; pub mod api; pub mod ws; pub static TIPS_PERCENTILE: LazyLock>> = LazyLock::new(|| RwLock::new(None)); #[derive(Debug, Deserialize, Clone)] pub struct TipPercentileData { pub time: String, pub landed_tips_25th_percentile: f64, pub landed_tips_50th_percentile: f64, pub landed_tips_75th_percentile: f64, pub landed_tips_95th_percentile: f64, pub landed_tips_99th_percentile: f64, pub ema_landed_tips_50th_percentile: f64, } pub static BLOCK_ENGINE_URL: LazyLock = LazyLock::new(|| get_env_var("JITO_BLOCK_ENGINE_URL")); pub static TIP_STREAM_URL: LazyLock = LazyLock::new(|| get_env_var("JITO_TIP_STREAM_URL")); pub static TIP_PERCENTILE: LazyLock = LazyLock::new(|| get_env_var("JITO_TIP_PERCENTILE")); pub static TIP_ACCOUNTS: LazyLock>> = LazyLock::new(|| RwLock::new(vec![])); pub async fn init_tip_accounts() -> Result<()> { let accounts: TipAccountResult = get_tip_accounts().await?.try_into()?; let mut tip_accounts = TIP_ACCOUNTS.write().await; accounts .accounts .iter() .for_each(|account| tip_accounts.push(account.to_string())); Ok(()) } pub async fn get_tip_account() -> Result { let accounts = TIP_ACCOUNTS.read().await; let mut rng = thread_rng(); match accounts.iter().choose(&mut rng) { Some(acc) => Ok(Pubkey::from_str(acc).inspect_err(|err| { error!("jito: failed to parse Pubkey: {:?}", err); })?), None => Err(anyhow!("jito: no tip accounts available")), } } pub async fn init_tip_amounts() -> Result<()> { let tip_percentiles = api::get_tip_amounts().await?; *TIPS_PERCENTILE.write().await = tip_percentiles.first().cloned(); Ok(()) } // unit sol pub async fn get_tip_value() -> Result { // If TIP_VALUE is set, use it if let Ok(tip_value) = std::env::var("JITO_TIP_VALUE") { if let Ok(value) = f64::from_str(&tip_value) { return Ok(value); } else { warn!( "Invalid TIP_VALUE in environment variable, falling back to percentile calculation" ); } } let tips = TIPS_PERCENTILE.read().await; if let Some(ref data) = *tips { match TIP_PERCENTILE.as_str() { "25" => Ok(data.landed_tips_25th_percentile), "50" => Ok(data.landed_tips_50th_percentile), "75" => Ok(data.landed_tips_75th_percentile), "95" => Ok(data.landed_tips_95th_percentile), "99" => Ok(data.landed_tips_99th_percentile), _ => Err(anyhow!("jito: invalid TIP_PERCENTILE value")), } } else { Err(anyhow!("jito: failed get tip")) } } #[derive(Deserialize, Debug)] pub struct BundleStatus { pub bundle_id: String, pub transactions: Vec, pub slot: u64, pub confirmation_status: String, pub err: ErrorStatus, } #[derive(Deserialize, Debug)] pub struct ErrorStatus { #[serde(rename = "Ok")] pub ok: Option<()>, } pub async fn wait_for_bundle_confirmation( fetch_statuses: F, bundle_id: String, interval: Duration, timeout: Duration, ) -> Result> where F: Fn(String) -> Fut, Fut: Future>>, { let progress_bar = new_progress_bar(); let start_time = Instant::now(); loop { let statuses = fetch_statuses(bundle_id.clone()).await?; if let Some(status) = statuses.first() { let bundle_status: BundleStatus = serde_json::from_value(status.clone()).inspect_err(|err| { error!( "Failed to parse JSON when get_bundle_statuses, err: {}", err, ); })?; debug!("{:?}", bundle_status); match bundle_status.confirmation_status.as_str() { "finalized" | "confirmed" => { progress_bar.finish_and_clear(); info!( "Finalized bundle {}: {}", bundle_id, bundle_status.confirmation_status ); // print tx bundle_status .transactions .iter() .for_each(|tx| info!("https://solscan.io/tx/{}", tx)); return Ok(bundle_status.transactions); } _ => { progress_bar.set_message(format!( "Finalizing bundle {}: {}", bundle_id, bundle_status.confirmation_status )); } } } else { progress_bar.set_message(format!("Finalizing bundle {}: {}", bundle_id, "None")); } // check loop exceeded 1 minute, if start_time.elapsed() > timeout { warn!("Loop exceeded {:?}, breaking out.", timeout); return Err(anyhow!("Bundle status get timeout")); } // Wait for a certain duration before retrying sleep(interval).await; } } pub fn new_progress_bar() -> ProgressBar { let progress_bar = ProgressBar::new(42); progress_bar.set_style( ProgressStyle::default_spinner() .template("{spinner:.green} {wide_msg}") .expect("ProgressStyle::template direct input to be correct"), ); progress_bar.enable_steady_tick(Duration::from_millis(100)); progress_bar } #[cfg(test)] mod tests { use std::time::Duration; use serde_json::{json, Value}; use super::wait_for_bundle_confirmation; fn generate_statuses(bundle_id: String, confirmation_status: &str) -> Vec { vec![json!({ "bundle_id": bundle_id, "transactions": ["tx1", "tx2"], "slot": 12345, "confirmation_status": confirmation_status, "err": {"Ok": null} })] } #[tokio::test] async fn test_success_confirmation() { for &status in &["finalized", "confirmed"] { let wait_result = wait_for_bundle_confirmation( |id| async { Ok(generate_statuses(id, status)) }, "6e4b90284778a40633b56e4289202ea79e62d2296bb3d45398bb93f6c9ec083d".to_string(), Duration::from_secs(1), Duration::from_secs(1), ) .await; assert!(wait_result.is_ok()); } } #[tokio::test] async fn test_error_confirmation() { let wait_result = wait_for_bundle_confirmation( |id| async { Ok(generate_statuses(id, "processed")) }, "6e4b90284778a40633b56e4289202ea79e62d2296bb3d45398bb93f6c9ec083d".to_string(), Duration::from_secs(1), Duration::from_secs(2), ) .await; assert!(wait_result.is_err()); } } ================================================ FILE: src/jito/ws.rs ================================================ use crate::jito::{TipPercentileData, TIPS_PERCENTILE, TIP_STREAM_URL}; use anyhow::{Context, Result}; use futures_util::StreamExt; use tokio_tungstenite::{connect_async, tungstenite::Message}; use tracing::{debug, error, info, warn}; pub async fn tip_stream() -> Result<()> { let (ws_stream, _) = connect_async(TIP_STREAM_URL.to_string()) .await .context("Failed to connect to WebSocket server")?; info!("Connected to WebSocket server: tip_stream"); let (mut _write, mut read) = ws_stream.split(); while let Some(message) = read.next().await { match message { Ok(Message::Text(text)) => { debug!("Received text message: {}", text); match serde_json::from_str::>(&text) { Ok(data) => { if !data.is_empty() { *TIPS_PERCENTILE.write().await = data.first().cloned(); } else { warn!("Received an empty data.") } } Err(e) => { error!("Failed to deserialize JSON: {:?}", e); } } } Ok(Message::Close(close)) => { info!("Connection closed: {:?}", close); break; } Err(e) => { error!("Error receiving message: {:?}", e); break; } _ => {} } } Ok(()) } ================================================ FILE: src/lib.rs ================================================ use std::{env, sync::Arc}; use anyhow::{anyhow, Result}; use rand::seq::SliceRandom; use reqwest::Proxy; use solana_client::rpc_client::RpcClient; use solana_sdk::signature::Keypair; use tracing::debug; pub mod api; pub mod constants; pub mod helper; pub mod jito; pub mod logger; pub mod pool; pub mod pump; pub mod raydium; pub mod swap; pub mod token; pub mod tx; fn get_env_var(key: &str) -> String { env::var(key).unwrap_or_else(|_| panic!("Environment variable {} is not set", key)) } pub fn get_client_build() -> Result { let mut client_builder = reqwest::Client::builder(); if let Ok(http_proxy) = env::var("HTTP_PROXY") { let proxy = Proxy::all(http_proxy)?; client_builder = client_builder.proxy(proxy); } match client_builder.build() { Ok(client) => Ok(client), Err(err) => Err(anyhow!("failed create client: {}", err)), } } pub fn get_random_rpc_url() -> Result { let cluster_urls = env::var("RPC_ENDPOINTS")? .split(",") .map(|s| s.trim().to_string()) .collect::>(); let random_url = cluster_urls .choose(&mut rand::thread_rng()) .expect("No RPC endpoints configured") .clone(); debug!("Choose rpc: {}", random_url); return Ok(random_url); } pub fn get_rpc_client() -> Result> { let random_url = get_random_rpc_url()?; let client = RpcClient::new(random_url); return Ok(Arc::new(client)); } pub fn get_wallet() -> Result> { let wallet = Keypair::from_base58_string(&env::var("PRIVATE_KEY")?); return Ok(Arc::new(wallet)); } #[cfg(test)] mod tests { #[ctor::ctor] fn init() { crate::logger::init(); dotenvy::dotenv().ok(); } } ================================================ FILE: src/logger.rs ================================================ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; pub fn init() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); } ================================================ FILE: src/main.rs ================================================ use anyhow::Result; use axum::{ http::{HeaderValue, Method}, routing::{get, post}, Router, }; use clap::{ArgGroup, Parser, Subcommand}; use raytx::{ api::{self, AppState}, get_rpc_client, get_wallet, jito, logger, raydium::get_pool_info, swap::{self, SwapDirection, SwapInType}, token, }; use std::{env, net::SocketAddr, str::FromStr}; use tower_http::cors::CorsLayer; use tracing::{debug, info}; use solana_sdk::{pubkey::Pubkey, signature::Signer}; #[derive(Parser)] #[command(name = "raytx", version, about, long_about = None)] struct Cli { #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Command { #[command(about = "swap the mint token")] #[command(group( ArgGroup::new("amount") .required(true) .args(&["amount_in", "amount_in_pct"]), ))] Swap { mint: String, #[arg(value_enum)] direction: SwapDirection, #[arg(long, help = "amount in")] amount_in: Option, #[arg(long, help = "amount in percentage, only support sell")] amount_in_pct: Option, #[arg(long, help = "use jito to swap", default_value_t = false)] jito: bool, }, Daemon { #[arg( long, help = "Start a long-running daemon process for swap", default_value = "127.0.0.1:7235" )] addr: String, }, #[command(about = "Wrap sol -> wsol")] Wrap {}, #[command(about = "Unwrap wsol -> sol")] Unwrap {}, #[command(subcommand)] Token(TokenCommand), } #[derive(Subcommand, Debug)] enum TokenCommand { #[command(about = "List your wallet token accounts")] List, #[command(about = "Show token account from arg mint")] Show { #[arg(help = "The mint address of the token")] mint: String, }, } #[tokio::main] async fn main() -> Result<()> { if let Ok(env_path) = env::var("DOTENV_PATH") { println!("Using env_path: {}", env_path); dotenvy::from_path(env_path).ok(); } else { dotenvy::dotenv().ok(); } let cli = Cli::parse(); logger::init(); let client = get_rpc_client()?; let wallet = get_wallet()?; let app_state = AppState { client, wallet }; match &cli.command { Some(Command::Swap { mint, direction, amount_in, amount_in_pct, jito, }) => { let (amount_in, in_type) = if let Some(amount_in) = amount_in { (amount_in, SwapInType::Qty) } else if let Some(amount_in) = amount_in_pct { (amount_in, SwapInType::Pct) } else { panic!("either in_amount or in_amount_pct must be provided"); }; let slippage = env::var("SLIPPAGE").unwrap_or("5".to_string()); let slippage = slippage.parse::().unwrap_or(5); debug!( "{} {:?} {:?} {:?} slippage: {}", mint, direction, amount_in, in_type, slippage ); // jito if *jito { jito::init_tip_accounts() .await .map_err(|err| { info!("failed to get tip accounts: {:?}", err); err }) .unwrap(); jito::init_tip_amounts() .await .map_err(|err| { info!("failed to init tip amounts: {:?}", err); err }) .unwrap(); } swap::swap( app_state, mint, *amount_in, direction.clone(), in_type, slippage, *jito, ) .await?; } Some(Command::Daemon { addr }) => { jito::init_tip_accounts().await.unwrap(); tokio::spawn(async { jito::ws::tip_stream() .await .expect("Failed to get tip percentiles data"); }); let app = Router::new() .nest( "/api", Router::new() .route("/swap", post(api::swap)) .route("/pool/:pool_id", get(api::get_pool)) .route("/coins/:mint", get(api::coins)) .route("/token_accounts", get(api::token_accounts)) .route("/token_accounts/:mint", get(api::token_account)) .with_state(app_state), ) .layer( CorsLayer::new() .allow_origin("*".parse::().unwrap()) .allow_methods([ Method::GET, Method::POST, Method::PUT, Method::OPTIONS, Method::DELETE, ]), ); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); info!("listening on {}", listener.local_addr().unwrap()); axum::serve( listener, app.into_make_service_with_connect_info::(), ) .await .unwrap(); } Some(Command::Token(token_command)) => match token_command { TokenCommand::List => { let token_accounts = token::token_accounts(&app_state.client, &app_state.wallet.pubkey()).await; info!("token_accounts: {:#?}", token_accounts); } TokenCommand::Show { mint } => { let mint = Pubkey::from_str(mint).expect("failed to parse mint pubkey"); let token_account = token::token_account(&app_state.client, &app_state.wallet.pubkey(), mint) .await?; info!("token_account: {:#?}", token_account); let pool_info = get_pool_info( &spl_token::native_mint::id().to_string(), &token_account.mint, ) .await?; let pool_id = pool_info.get_pool().unwrap().id; info!("pool id: {}", pool_id); } }, _ => {} } Ok(()) } ================================================ FILE: src/pool.rs ================================================ use anyhow::Result; use common::common_utils; use spl_token_2022::amount_to_ui_amount; use tracing::{debug, warn}; use crate::{ helper::get_solana_price, raydium::{get_pool_state, Raydium}, }; impl Raydium { pub async fn get_pool(&self, pool_id: &str) -> Result<(f64, f64, f64, f64, f64)> { let (base, quote, price) = self.get_pool_price(Some(pool_id), None).await?; let sol_price = get_solana_price() .await .inspect_err(|err| warn!("failed get solana price: {}", err))?; let usd_price = ((price * sol_price) * 1_000_000_000.0).round() / 1_000_000_000.0; debug!("sol price: {}, usd_price: {} ", sol_price, usd_price); Ok((base, quote, price, usd_price, sol_price)) } pub async fn get_pool_price( &self, pool_id: Option<&str>, mint: Option<&str>, ) -> Result<(f64, f64, f64)> { let (amm_pool_id, pool_state) = get_pool_state(self.client.clone(), pool_id, mint).await?; // debug!("pool_state : {:#?}", pool_state); let load_pubkeys = vec![pool_state.pc_vault, pool_state.coin_vault]; let rsps = common::rpc::get_multiple_accounts(&self.client, &load_pubkeys).unwrap(); let amm_pc_vault_account = rsps[0].clone(); let amm_coin_vault_account = rsps[1].clone(); let amm_pc_vault = common_utils::unpack_token(&amm_pc_vault_account.as_ref().unwrap().data).unwrap(); let amm_coin_vault = common_utils::unpack_token(&amm_coin_vault_account.as_ref().unwrap().data).unwrap(); let (base_account, quote_account) = if amm_coin_vault.base.is_native() { ( ( pool_state.pc_vault_mint, amount_to_ui_amount(amm_pc_vault.base.amount, pool_state.pc_decimals as u8), ), ( pool_state.coin_vault_mint, amount_to_ui_amount(amm_coin_vault.base.amount, pool_state.coin_decimals as u8), ), ) } else { ( ( pool_state.coin_vault_mint, amount_to_ui_amount(amm_coin_vault.base.amount, pool_state.coin_decimals as u8), ), ( pool_state.pc_vault_mint, amount_to_ui_amount(amm_pc_vault.base.amount, pool_state.pc_decimals as u8), ), ) }; let price = quote_account.1 / base_account.1; debug!( "calculate pool[{}]: {}: {}, {}: {}, price: {} sol", amm_pool_id, base_account.0, base_account.1, quote_account.0, quote_account.1, price ); Ok((base_account.1, quote_account.1, price)) } } ================================================ FILE: src/pump.rs ================================================ use std::{str::FromStr, sync::Arc}; use anyhow::{anyhow, Result}; use borsh::from_slice; use borsh_derive::{BorshDeserialize, BorshSerialize}; use raydium_amm::math::U128; use serde::{Deserialize, Serialize}; use solana_client::rpc_client::RpcClient; use solana_sdk::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, signature::Keypair, signer::Signer, system_program, }; use spl_associated_token_account::{ get_associated_token_address, instruction::create_associated_token_account, }; use spl_token::{amount_to_ui_amount, ui_amount_to_amount}; use spl_token_client::token::TokenError; use tracing::{debug, error, info, warn}; use crate::{ swap::{SwapDirection, SwapInType}, token, tx, }; pub const TEN_THOUSAND: u64 = 10000; pub const TOKEN_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; pub const RENT_PROGRAM: &str = "SysvarRent111111111111111111111111111111111"; pub const ASSOCIATED_TOKEN_PROGRAM: &str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; pub const PUMP_GLOBAL: &str = "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"; pub const PUMP_FEE_RECIPIENT: &str = "62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"; pub const PUMP_PROGRAM: &str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; // pub const PUMP_FUN_MINT_AUTHORITY: &str = "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"; pub const PUMP_ACCOUNT: &str = "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"; pub const PUMP_BUY_METHOD: u64 = 16927863322537952870; pub const PUMP_SELL_METHOD: u64 = 12502976635542562355; pub struct Pump { pub client: Arc, pub keypair: Arc, } impl Pump { pub fn new(client: Arc, keypair: Arc) -> Self { Self { client, keypair } } pub async fn swap( &self, mint: &str, amount_in: f64, swap_direction: SwapDirection, in_type: SwapInType, slippage: u64, use_jito: bool, ) -> Result> { // slippage_bps = 50u64; // 0.5% let slippage_bps = slippage * 100; let owner = self.keypair.pubkey(); let mint = Pubkey::from_str(mint).map_err(|e| anyhow!("failed to parse mint pubkey: {}", e))?; let program_id = spl_token::ID; let native_mint = spl_token::native_mint::ID; let (token_in, token_out, pump_method) = match swap_direction { SwapDirection::Buy => (native_mint, mint, PUMP_BUY_METHOD), SwapDirection::Sell => (mint, native_mint, PUMP_SELL_METHOD), }; let pump_program = Pubkey::from_str(PUMP_PROGRAM)?; let (bonding_curve, associated_bonding_curve, bonding_curve_account) = get_bonding_curve_account(self.client.clone(), &mint, &pump_program).await?; let in_ata = get_associated_token_address(&owner, &token_in); let out_ata = get_associated_token_address(&owner, &token_out); let mut create_instruction = None; let mut close_instruction = None; let (amount_specified, amount_ui_pretty) = match swap_direction { SwapDirection::Buy => { // Create base ATA if it doesn't exist. match token::get_account_info( self.client.clone(), self.keypair.clone(), &token_out, &out_ata, ) .await { Ok(_) => debug!("base ata exists. skipping creation.."), Err(TokenError::AccountNotFound) | Err(TokenError::AccountInvalidOwner) => { info!( "base ATA for mint {} does not exist. will be create", token_out ); create_instruction = Some(create_associated_token_account( &owner, &owner, &token_out, &program_id, )); } Err(error) => error!("error retrieving out ATA: {}", error), } ( ui_amount_to_amount(amount_in, spl_token::native_mint::DECIMALS), (amount_in, spl_token::native_mint::DECIMALS), ) } SwapDirection::Sell => { let in_account = token::get_account_info( self.client.clone(), self.keypair.clone(), &token_in, &in_ata, ) .await?; let in_mint = token::get_mint_info(self.client.clone(), self.keypair.clone(), &token_in) .await?; let amount = match in_type { SwapInType::Qty => ui_amount_to_amount(amount_in, in_mint.base.decimals), SwapInType::Pct => { let amount_in_pct = amount_in.min(1.0); if amount_in_pct == 1.0 { // sell all, close ata info!("sell all. will be close ATA for mint {}", token_in); close_instruction = Some(spl_token::instruction::close_account( &program_id, &in_ata, &owner, &owner, &vec![&owner], )?); in_account.base.amount } else { (amount_in_pct * 100.0) as u64 * in_account.base.amount / 100 } } }; ( amount, ( amount_to_ui_amount(amount, in_mint.base.decimals), in_mint.base.decimals, ), ) } }; info!( "swap: {}, value: {:?} -> {}", token_in, amount_ui_pretty, token_out ); // Calculate tokens out let virtual_sol_reserves = U128::from(bonding_curve_account.virtual_sol_reserves); let virtual_token_reserves = U128::from(bonding_curve_account.virtual_token_reserves); let unit_price = (bonding_curve_account.virtual_sol_reserves as f64 / bonding_curve_account.virtual_token_reserves as f64) / 1000.0; let creator = Pubkey::new_from_array(bonding_curve_account.creator); let creator_vault = get_creator_vault_pda(&creator, &pump_program)?; let (token_amount, sol_amount_threshold, input_accouts) = match swap_direction { SwapDirection::Buy => { let max_sol_cost = max_amount_with_slippage(amount_specified, slippage_bps); ( U128::from(amount_specified) .checked_mul(virtual_token_reserves) .unwrap() .checked_div(virtual_sol_reserves) .unwrap() .as_u64(), max_sol_cost, vec![ AccountMeta::new_readonly(Pubkey::from_str(PUMP_GLOBAL)?, false), AccountMeta::new(Pubkey::from_str(PUMP_FEE_RECIPIENT)?, false), AccountMeta::new_readonly(mint, false), AccountMeta::new(bonding_curve, false), AccountMeta::new(associated_bonding_curve, false), AccountMeta::new(out_ata, false), AccountMeta::new(owner, true), AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(program_id, false), AccountMeta::new(creator_vault, false), AccountMeta::new_readonly(Pubkey::from_str(PUMP_ACCOUNT)?, false), AccountMeta::new_readonly(pump_program, false), ], ) } SwapDirection::Sell => { let sol_output = U128::from(amount_specified) .checked_mul(virtual_sol_reserves) .unwrap() .checked_div(virtual_token_reserves) .unwrap() .as_u64(); let min_sol_output = min_amount_with_slippage(sol_output, slippage_bps); ( amount_specified, min_sol_output, vec![ AccountMeta::new_readonly(Pubkey::from_str(PUMP_GLOBAL)?, false), AccountMeta::new(Pubkey::from_str(PUMP_FEE_RECIPIENT)?, false), AccountMeta::new_readonly(mint, false), AccountMeta::new(bonding_curve, false), AccountMeta::new(associated_bonding_curve, false), AccountMeta::new(in_ata, false), AccountMeta::new(owner, true), AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(creator_vault, false), AccountMeta::new_readonly(program_id, false), AccountMeta::new_readonly(Pubkey::from_str(PUMP_ACCOUNT)?, false), AccountMeta::new_readonly(pump_program, false), ], ) } }; info!( "token_amount: {}, sol_amount_threshold: {}, unit_price: {} sol", token_amount, sol_amount_threshold, unit_price ); let build_swap_instruction = Instruction::new_with_bincode( pump_program, &(pump_method, token_amount, sol_amount_threshold), input_accouts, ); // build instructions let mut instructions = vec![]; if let Some(create_instruction) = create_instruction { instructions.push(create_instruction); } if amount_specified > 0 { instructions.push(build_swap_instruction) } if let Some(close_instruction) = close_instruction { instructions.push(close_instruction); } if instructions.len() == 0 { return Err(anyhow!("instructions is empty, no tx required")); } tx::new_signed_and_send(&self.client, &self.keypair, instructions, use_jito).await } } fn min_amount_with_slippage(input_amount: u64, slippage_bps: u64) -> u64 { input_amount .checked_mul(TEN_THOUSAND.checked_sub(slippage_bps).unwrap()) .unwrap() .checked_div(TEN_THOUSAND) .unwrap() } fn max_amount_with_slippage(input_amount: u64, slippage_bps: u64) -> u64 { input_amount .checked_mul(slippage_bps.checked_add(TEN_THOUSAND).unwrap()) .unwrap() .checked_div(TEN_THOUSAND) .unwrap() } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RaydiumInfo { pub base: f64, pub quote: f64, pub price: f64, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PumpInfo { pub mint: String, pub bonding_curve: String, pub associated_bonding_curve: String, pub raydium_pool: Option, pub raydium_info: Option, pub complete: bool, pub virtual_sol_reserves: u64, pub virtual_token_reserves: u64, pub total_supply: u64, } #[derive(Debug, BorshSerialize, BorshDeserialize)] pub struct BondingCurveAccount { pub discriminator: u64, pub virtual_token_reserves: u64, pub virtual_sol_reserves: u64, pub real_token_reserves: u64, pub real_sol_reserves: u64, pub token_total_supply: u64, pub complete: bool, pub creator: [u8; 32], } pub async fn get_bonding_curve_account( rpc_client: Arc, mint: &Pubkey, program_id: &Pubkey, ) -> Result<(Pubkey, Pubkey, BondingCurveAccount)> { let bonding_curve = get_pda(mint, program_id)?; let associated_bonding_curve = get_associated_token_address(&bonding_curve, &mint); let bonding_curve_data = rpc_client .get_account_data(&bonding_curve) .inspect_err(|err| { warn!( "Failed to get bonding curve account data: {}, err: {}", bonding_curve, err ); })?; let bonding_curve_account = from_slice::(&bonding_curve_data[..81]) .map_err(|e| { anyhow!( "Failed to deserialize bonding curve account: {}", e.to_string() ) })?; // println!("{:?}", bonding_curve_account); Ok(( bonding_curve, associated_bonding_curve, bonding_curve_account, )) } pub fn get_pda(mint: &Pubkey, program_id: &Pubkey) -> Result { let seeds = [b"bonding-curve".as_ref(), mint.as_ref()]; let (bonding_curve, _bump) = Pubkey::find_program_address(&seeds, program_id); Ok(bonding_curve) } pub fn get_creator_vault_pda(creator: &Pubkey, program_id: &Pubkey) -> Result { let seeds = [b"creator-vault".as_ref(), creator.as_ref()]; let (creator_vault, _bump) = Pubkey::find_program_address(&seeds, program_id); Ok(creator_vault) } // https://frontend-api.pump.fun/coins/8zSLdDzM1XsqnfrHmHvA9ir6pvYDjs8UXz6B2Tydd6b2 pub async fn get_pump_info( rpc_client: Arc, mint: &str, ) -> Result { let mint = Pubkey::from_str(mint)?; let program_id = Pubkey::from_str(PUMP_PROGRAM)?; let (bonding_curve, associated_bonding_curve, bonding_curve_account) = get_bonding_curve_account(rpc_client, &mint, &program_id).await?; let pump_info = PumpInfo { mint: mint.to_string(), bonding_curve: bonding_curve.to_string(), associated_bonding_curve: associated_bonding_curve.to_string(), raydium_pool: None, raydium_info: None, complete: bonding_curve_account.complete, virtual_sol_reserves: bonding_curve_account.virtual_sol_reserves, virtual_token_reserves: bonding_curve_account.virtual_token_reserves, total_supply: bonding_curve_account.token_total_supply, }; Ok(pump_info) } ================================================ FILE: src/raydium.rs ================================================ use std::env; use amm_cli::AmmSwapInfoResult; use anyhow::{anyhow, Context, Result}; use raydium_amm::state::{AmmInfo, Loadable}; use reqwest::Proxy; use serde::Deserialize; use solana_client::{ rpc_client::RpcClient, rpc_filter::{Memcmp, RpcFilterType}, }; use solana_sdk::{ // native_token::LAMPORTS_PER_SOL, instruction::Instruction, program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, }; use spl_associated_token_account::{ get_associated_token_address, instruction::create_associated_token_account, }; use spl_token::{amount_to_ui_amount, ui_amount_to_amount}; use spl_token_client::token::TokenError; use std::{str::FromStr, sync::Arc}; use crate::{ swap::{SwapDirection, SwapInType}, token, tx, }; use spl_token::state::Account; use tracing::{debug, error, info}; pub const AMM_PROGRAM: &str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"; pub struct Raydium { pub client: Arc, pub keypair: Arc, pub pool_id: Option, } impl Raydium { pub fn new(client: Arc, keypair: Arc) -> Self { Self { client, keypair, pool_id: None, } } pub fn with_pool_id(&mut self, pool_id: Option) -> &mut Self { self.pool_id = pool_id; self } pub async fn swap( &self, mint_str: &str, amount_in: f64, swap_direction: SwapDirection, in_type: SwapInType, slippage: u64, use_jito: bool, ) -> Result> { // slippage_bps = 50u64; // 0.5% let slippage_bps = slippage * 100; let owner = self.keypair.pubkey(); let mint = Pubkey::from_str(mint_str) .map_err(|e| anyhow!("failed to parse mint pubkey: {}", e))?; let program_id = spl_token::ID; let native_mint = spl_token::native_mint::ID; let (amm_pool_id, pool_state) = get_pool_state(self.client.clone(), self.pool_id.as_deref(), Some(mint_str)).await?; // debug!("pool_state: {:#?}", pool_state); let (token_in, token_out, user_input_token, swap_base_in) = match ( swap_direction.clone(), pool_state.coin_vault_mint == native_mint, ) { (SwapDirection::Buy, true) => (native_mint, mint, pool_state.coin_vault, true), (SwapDirection::Buy, false) => (native_mint, mint, pool_state.pc_vault, true), (SwapDirection::Sell, true) => (mint, native_mint, pool_state.pc_vault, true), (SwapDirection::Sell, false) => (mint, native_mint, pool_state.coin_vault, true), }; debug!("token_in:{token_in}, token_out:{token_out}, user_input_token:{user_input_token}, swap_base_in:{swap_base_in}"); let in_ata = get_associated_token_address(&owner, &token_in); let out_ata = get_associated_token_address(&owner, &token_out); let mut create_instruction = None; let mut close_instruction = None; let (amount_specified, amount_ui_pretty) = match swap_direction { SwapDirection::Buy => { // Create base ATA if it doesn't exist. match token::get_account_info( self.client.clone(), self.keypair.clone(), &token_out, &out_ata, ) .await { Ok(_) => debug!("base ata exists. skipping creation.."), Err(TokenError::AccountNotFound) | Err(TokenError::AccountInvalidOwner) => { info!( "base ATA for mint {} does not exist. will be create", token_out ); // token::create_associated_token_account( // self.client.clone(), // self.keypair.clone(), // &token_out, // &owner, // ) // .await?; create_instruction = Some(create_associated_token_account( &owner, &owner, &token_out, &program_id, )); } Err(error) => error!("error retrieving out ATA: {}", error), } ( ui_amount_to_amount(amount_in, spl_token::native_mint::DECIMALS), (amount_in, spl_token::native_mint::DECIMALS), ) } SwapDirection::Sell => { let in_account = token::get_account_info( self.client.clone(), self.keypair.clone(), &token_in, &in_ata, ) .await?; let in_mint = token::get_mint_info(self.client.clone(), self.keypair.clone(), &token_in) .await?; let amount = match in_type { SwapInType::Qty => ui_amount_to_amount(amount_in, in_mint.base.decimals), SwapInType::Pct => { let amount_in_pct = amount_in.min(1.0); if amount_in_pct == 1.0 { // sell all, close ata info!("sell all. will be close ATA for mint {}", token_in); close_instruction = Some(spl_token::instruction::close_account( &program_id, &in_ata, &owner, &owner, &vec![&owner], )?); in_account.base.amount } else { (amount_in_pct * 100.0) as u64 * in_account.base.amount / 100 } } }; ( amount, ( amount_to_ui_amount(amount, in_mint.base.decimals), in_mint.base.decimals, ), ) } }; let amm_program = Pubkey::from_str(AMM_PROGRAM)?; debug!("amm pool id: {amm_pool_id}"); let swap_info_result = amm_cli::calculate_swap_info( &self.client, amm_program, amm_pool_id, user_input_token, amount_specified, slippage_bps, swap_base_in, )?; let other_amount_threshold = swap_info_result.other_amount_threshold; info!("swap_info_result: {:#?}", swap_info_result); info!( "swap: {}, value: {:?} -> {}", token_in, amount_ui_pretty, token_out ); // build instructions let mut instructions = vec![]; // sol <-> wsol support let mut wsol_account = None; if token_in == native_mint || token_out == native_mint { // create wsol account let seed = &format!("{}", Keypair::new().pubkey())[..32]; let wsol_pubkey = Pubkey::create_with_seed(&owner, seed, &spl_token::id())?; wsol_account = Some(wsol_pubkey); // LAMPORTS_PER_SOL / 100 // 0.01 SOL as rent // get rent let rent = self .client .get_minimum_balance_for_rent_exemption(Account::LEN)?; // if buy add amount_specified let total_amount = if token_in == native_mint { rent + amount_specified } else { rent }; // create tmp wsol account instructions.push(system_instruction::create_account_with_seed( &owner, &wsol_pubkey, &owner, seed, total_amount, Account::LEN as u64, // 165, // Token account size &spl_token::id(), )); // initialize account instructions.push(spl_token::instruction::initialize_account( &spl_token::id(), &wsol_pubkey, &native_mint, &owner, )?); } if let Some(create_instruction) = create_instruction { instructions.push(create_instruction); } if amount_specified > 0 { let mut close_wsol_account_instruction = None; // replace native mint with tmp wsol account let mut final_in_ata = in_ata; let mut final_out_ata = out_ata; if let Some(wsol_account) = wsol_account { match swap_direction { SwapDirection::Buy => { final_in_ata = wsol_account; } SwapDirection::Sell => { final_out_ata = wsol_account; } } close_wsol_account_instruction = Some(spl_token::instruction::close_account( &program_id, &wsol_account, &owner, &owner, &vec![&owner], )?); } // build swap instruction let build_swap_instruction = amm_swap( &amm_program, swap_info_result, &owner, &final_in_ata, &final_out_ata, amount_specified, other_amount_threshold, swap_base_in, )?; info!( "amount_specified: {}, other_amount_threshold: {}, wsol_account: {:?}", amount_specified, other_amount_threshold, wsol_account ); instructions.push(build_swap_instruction); // close wsol account if let Some(close_wsol_account_instruction) = close_wsol_account_instruction { instructions.push(close_wsol_account_instruction); } } if let Some(close_instruction) = close_instruction { instructions.push(close_instruction); } if instructions.len() == 0 { return Err(anyhow!("instructions is empty, no tx required")); } tx::new_signed_and_send(&self.client, &self.keypair, instructions, use_jito).await } } pub fn amm_swap( amm_program: &Pubkey, result: AmmSwapInfoResult, user_owner: &Pubkey, user_source: &Pubkey, user_destination: &Pubkey, amount_specified: u64, other_amount_threshold: u64, swap_base_in: bool, ) -> Result { let swap_instruction = if swap_base_in { raydium_amm::instruction::swap_base_in( &amm_program, &result.pool_id, &result.amm_authority, &result.amm_open_orders, &result.amm_coin_vault, &result.amm_pc_vault, &result.market_program, &result.market, &result.market_bids, &result.market_asks, &result.market_event_queue, &result.market_coin_vault, &result.market_pc_vault, &result.market_vault_signer, user_source, user_destination, user_owner, amount_specified, other_amount_threshold, )? } else { raydium_amm::instruction::swap_base_out( &amm_program, &result.pool_id, &result.amm_authority, &result.amm_open_orders, &result.amm_coin_vault, &result.amm_pc_vault, &result.market_program, &result.market, &result.market_bids, &result.market_asks, &result.market_event_queue, &result.market_coin_vault, &result.market_pc_vault, &result.market_vault_signer, user_source, user_destination, user_owner, other_amount_threshold, amount_specified, )? }; Ok(swap_instruction) } pub async fn get_pool_state( rpc_client: Arc, pool_id: Option<&str>, mint: Option<&str>, ) -> Result<(Pubkey, AmmInfo)> { if let Some(pool_id) = pool_id { debug!("finding pool state by pool_id: {}", pool_id); let amm_pool_id = Pubkey::from_str(pool_id)?; let pool_state = common::rpc::get_account::(&rpc_client, &amm_pool_id)? .ok_or(anyhow!("NotFoundPool: pool state not found"))?; Ok((amm_pool_id, pool_state)) } else { if let Some(mint) = mint { // find pool by mint via rpc if let Ok(pool_state) = get_pool_state_by_mint(rpc_client.clone(), mint).await { return Ok(pool_state); } // find pool by mint via raydium api let pool_data = get_pool_info(&spl_token::native_mint::ID.to_string(), mint).await; if let Ok(pool_data) = pool_data { let pool = pool_data .get_pool() .ok_or(anyhow!("NotFoundPool: pool not found in raydium api"))?; let amm_pool_id = Pubkey::from_str(&pool.id)?; debug!("finding pool state by raydium api: {}", amm_pool_id); let pool_state = common::rpc::get_account::( &rpc_client, &amm_pool_id, )? .ok_or(anyhow!("NotFoundPool: pool state not found"))?; return Ok((amm_pool_id, pool_state)); } Err(anyhow!("NotFoundPool: pool state not found")) } else { Err(anyhow!("NotFoundPool: pool state not found")) } } } pub async fn get_pool_state_by_mint( rpc_client: Arc, mint: &str, ) -> Result<(Pubkey, AmmInfo)> { debug!("finding pool state by mint: {}", mint); // (pc_mint, coin_mint) let pairs = vec![ // pump pool ( Some(spl_token::native_mint::ID), Pubkey::from_str(mint).ok(), ), // general pool ( Pubkey::from_str(mint).ok(), Some(spl_token::native_mint::ID), ), ]; let pool_len = core::mem::size_of::() as u64; let amm_program = Pubkey::from_str(AMM_PROGRAM)?; // Find matching AMM pool from mint pairs by filter let mut found_pools = None; for (coin_mint, pc_mint) in pairs { debug!( "get_pool_state_by_mint filter: coin_mint: {:?}, pc_mint: {:?}", coin_mint, pc_mint ); let filters = match (coin_mint, pc_mint) { (None, None) => Some(vec![RpcFilterType::DataSize(pool_len)]), (Some(coin_mint), None) => Some(vec![ RpcFilterType::Memcmp(Memcmp::new_base58_encoded(400, &coin_mint.to_bytes())), RpcFilterType::DataSize(pool_len), ]), (None, Some(pc_mint)) => Some(vec![ RpcFilterType::Memcmp(Memcmp::new_base58_encoded(432, &pc_mint.to_bytes())), RpcFilterType::DataSize(pool_len), ]), (Some(coin_mint), Some(pc_mint)) => Some(vec![ RpcFilterType::Memcmp(Memcmp::new_base58_encoded(400, &coin_mint.to_bytes())), RpcFilterType::Memcmp(Memcmp::new_base58_encoded(432, &pc_mint.to_bytes())), RpcFilterType::DataSize(pool_len), ]), }; let pools = common::rpc::get_program_accounts_with_filters(&rpc_client, amm_program, filters) .unwrap(); if !pools.is_empty() { found_pools = Some(pools); break; } } match found_pools { Some(pools) => { let pool = &pools[0]; let pool_state = raydium_amm::state::AmmInfo::load_from_bytes(&pools[0].1.data)?; Ok((pool.0, pool_state.clone())) } None => { return Err(anyhow!("NotFoundPool: pool state not found")); } } } // get pool info // https://api-v3.raydium.io/pools/info/mint?mint1=So11111111111111111111111111111111111111112&mint2=EzM2d8JVpzfhV7km3tUsR1U1S4xwkrPnWkM4QFeTpump&poolType=standard&poolSortField=default&sortType=desc&pageSize=10&page=1 pub async fn get_pool_info(mint1: &str, mint2: &str) -> Result { let mut client_builder = reqwest::Client::builder(); if let Ok(http_proxy) = env::var("HTTP_PROXY") { let proxy = Proxy::all(http_proxy)?; client_builder = client_builder.proxy(proxy); } let client = client_builder.build()?; let result = client .get("https://api-v3.raydium.io/pools/info/mint") .query(&[ ("mint1", mint1), ("mint2", mint2), ("poolType", "standard"), ("poolSortField", "default"), ("sortType", "desc"), ("pageSize", "1"), ("page", "1"), ]) .send() .await? .json::() .await .context("Failed to parse pool info JSON")?; Ok(result.data) } // get pool info by ids // https://api-v3.raydium.io/pools/info/ids?ids=3RHg85W1JtKeqFQSxBfd2RX13aBFvvy6gcATkHU657mL pub async fn get_pool_info_by_id(pool_id: &str) -> Result { let mut client_builder = reqwest::Client::builder(); if let Ok(http_proxy) = env::var("HTTP_PROXY") { let proxy = Proxy::all(http_proxy)?; client_builder = client_builder.proxy(proxy); } let client = client_builder.build()?; let result = client .get("https://api-v3.raydium.io/pools/info/ids") .query(&[("ids", pool_id)]) .send() .await? .json::() .await .context("Failed to parse pool info JSON")?; Ok(result) } #[derive(Debug, Deserialize)] pub struct PoolInfo { pub success: bool, pub data: PoolData, } #[derive(Debug, Deserialize)] pub struct PoolData { // pub count: u32, pub data: Vec, } impl PoolData { pub fn get_pool(&self) -> Option { self.data.first().cloned() } } #[derive(Debug, Deserialize, Clone)] pub struct Pool { pub id: String, #[serde(rename = "programId")] pub program_id: String, #[serde(rename = "mintA")] pub mint_a: Mint, #[serde(rename = "mintB")] pub mint_b: Mint, #[serde(rename = "marketId")] pub market_id: String, } #[derive(Debug, Deserialize, Clone)] pub struct Mint { pub address: String, pub symbol: String, pub name: String, pub decimals: u8, } ================================================ FILE: src/swap.rs ================================================ use anyhow::Result; use clap::ValueEnum; use serde::Deserialize; use tracing::info; use crate::{ api::AppState, get_rpc_client, pump::{self, get_pump_info}, raydium, }; #[derive(ValueEnum, Debug, Clone, Deserialize)] pub enum SwapDirection { #[serde(rename = "buy")] Buy, #[serde(rename = "sell")] Sell, } impl From for u8 { fn from(value: SwapDirection) -> Self { match value { SwapDirection::Buy => 0, SwapDirection::Sell => 1, } } } #[derive(ValueEnum, Debug, Clone, Deserialize)] pub enum SwapInType { /// Quantity #[serde(rename = "qty")] Qty, /// Percentage #[serde(rename = "pct")] Pct, } pub async fn swap( state: AppState, mint: &str, amount_in: f64, swap_direction: SwapDirection, in_type: SwapInType, slippage: u64, use_jito: bool, ) -> Result> { let client = get_rpc_client()?; let wallet = state.wallet; let pump_info_result = get_pump_info(client.clone(), mint).await; println!("pump_info_result: {:#?}", pump_info_result); match pump_info_result { Ok(pump_info) => { if !pump_info.complete { // Pump token not completed, use original pump trading info!("swap in pump fun"); let swapx = pump::Pump::new(client, wallet); swapx .swap(mint, amount_in, swap_direction, in_type, slippage, use_jito) .await } else { // Pump token completed, use pump amm trading // info!("swap in pump amm"); Err(anyhow::anyhow!( "Pump token {} is completed, not support swap in pump amm yet", mint )) } } Err(_err) => { // Not a pump token or failed to get pump info, use raydium info!("swap in raydium"); let swapx = raydium::Raydium::new(client, wallet); swapx .swap(mint, amount_in, swap_direction, in_type, slippage, use_jito) .await } } } ================================================ FILE: src/token.rs ================================================ use std::sync::Arc; use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; use solana_account_decoder::UiAccountData; use solana_client::{rpc_client::RpcClient, rpc_request::TokenAccountsFilter}; use solana_sdk::{pubkey::Pubkey, signature::Keypair}; use spl_token_2022::{ extension::StateWithExtensionsOwned, state::{Account, Mint}, }; use spl_token_client::{ client::{ProgramClient, ProgramRpcClient, ProgramRpcClientSendTransaction}, token::{TokenError, TokenResult}, }; use tracing::{trace, warn}; pub type TokenAccounts = Vec; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TokenAccount { pub pubkey: String, pub mint: String, pub amount: String, pub ui_amount: f64, } #[derive(Debug, Serialize, Deserialize)] struct ParsedAccount { program: String, parsed: Parsed, space: u64, } #[derive(Debug, Serialize, Deserialize)] struct Parsed { info: TokenInfo, #[serde(rename = "type")] account_type: String, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct TokenInfo { is_native: bool, mint: String, owner: String, state: String, token_amount: Amount, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct Amount { amount: String, decimals: u8, ui_amount: f64, ui_amount_string: String, } pub async fn token_account( client: &RpcClient, owner: &Pubkey, mint: Pubkey, ) -> Result { let token_accounts = token_accounts_filter(client, owner, TokenAccountsFilter::Mint(mint)).await?; token_accounts .first() .cloned() .ok_or(anyhow!("NotFound: token account not found")) } pub async fn token_accounts(client: &RpcClient, owner: &Pubkey) -> Result { token_accounts_filter( client, owner, TokenAccountsFilter::ProgramId(spl_token::id()), ) .await } async fn token_accounts_filter( client: &RpcClient, owner: &Pubkey, filter: TokenAccountsFilter, ) -> Result { let token_accounts = client .get_inner_client() .get_token_accounts_by_owner(owner, filter) .await .expect("Failed to get token accounts"); trace!("token_accounts: {:#?}", token_accounts); let mut tas: TokenAccounts = vec![]; for token_account in token_accounts.into_iter() { let account_data = token_account.account.data; match account_data { UiAccountData::Json(parsed_account) => { let parsed: Parsed = serde_json::from_value(parsed_account.parsed)?; tas.push(TokenAccount { pubkey: token_account.pubkey, mint: parsed.info.mint, amount: parsed.info.token_amount.amount, ui_amount: parsed.info.token_amount.ui_amount, }); } UiAccountData::LegacyBinary(_) | UiAccountData::Binary(_, _) => { continue; } } } Ok(tas) } pub async fn get_account_info( client: Arc, _keypair: Arc, address: &Pubkey, account: &Pubkey, ) -> TokenResult> { let program_client = Arc::new(ProgramRpcClient::new( client.get_inner_client().clone(), ProgramRpcClientSendTransaction, )); let account = program_client .get_account(*account) .await .map_err(TokenError::Client)? .ok_or(TokenError::AccountNotFound) .inspect_err(|err| warn!("{} {}: mint {}", account, err, address))?; if account.owner != spl_token::ID { return Err(TokenError::AccountInvalidOwner); } let account = StateWithExtensionsOwned::::unpack(account.data)?; if account.base.mint != *address { return Err(TokenError::AccountInvalidMint); } Ok(account) } // pub async fn get_account_info( // client: Arc, // keypair: Arc, // address: &Pubkey, // account: &Pubkey, // ) -> TokenResult> { // let token_client = Token::new( // Arc::new(ProgramRpcClient::new( // client.clone(), // ProgramRpcClientSendTransaction, // )), // &spl_token::ID, // address, // None, // Arc::new(Keypair::from_bytes(&keypair.to_bytes()).expect("failed to copy keypair")), // ); // token_client.get_account_info(account).await // } pub async fn get_mint_info( client: Arc, _keypair: Arc, address: &Pubkey, ) -> TokenResult> { let program_client = Arc::new(ProgramRpcClient::new( client.get_inner_client().clone(), ProgramRpcClientSendTransaction, )); let account = program_client .get_account(*address) .await .map_err(TokenError::Client)? .ok_or(TokenError::AccountNotFound) .inspect_err(|err| warn!("{} {}: mint {}", address, err, address))?; if account.owner != spl_token::ID { return Err(TokenError::AccountInvalidOwner); } let mint_result = StateWithExtensionsOwned::::unpack(account.data).map_err(Into::into); let decimals: Option = None; if let (Ok(mint), Some(decimals)) = (&mint_result, decimals) { if decimals != mint.base.decimals { return Err(TokenError::InvalidDecimals); } } mint_result } // pub async fn get_mint_info( // client: Arc, // keypair: Arc, // address: &Pubkey, // ) -> TokenResult> { // let token_client = Token::new( // Arc::new(ProgramRpcClient::new( // client.clone(), // ProgramRpcClientSendTransaction, // )), // &spl_token::ID, // address, // None, // Arc::new(Keypair::from_bytes(&keypair.to_bytes()).expect("failed to copy keypair")), // ); // token_client.get_mint_info().await // } #[cfg(test)] mod tests { #[cfg(feature = "slow_tests")] mod slow_tests { use crate::{get_rpc_client, token::token_account}; use solana_sdk::pubkey::Pubkey; use std::str::FromStr; #[tokio::test] pub async fn test_token_account() { let client = get_rpc_client().unwrap(); let owner = Pubkey::from_str("AAf6DN1Wkh4TKvqxVX1xLfEKRtZNSZKwrHsr3NL2Wphm") .expect("failed to parse owner pubkey"); let mint = spl_token::native_mint::id(); let token_account = token_account(&client, &owner, mint).await.unwrap(); assert_eq!( token_account.pubkey, "C4rpfuopbU2q8kmn9panVsi2NkXW2uQaubmFSx9XCi1H" ) } } } ================================================ FILE: src/tx.rs ================================================ use std::{env, sync::Arc, time::Duration}; use anyhow::{anyhow, Result}; use jito_json_rpc_client::jsonrpc_client::rpc_client::RpcClient as JitoRpcClient; use solana_client::rpc_client::RpcClient; use solana_sdk::{ instruction::Instruction, signature::Keypair, signer::Signer, system_transaction, transaction::{Transaction, VersionedTransaction}, }; use spl_token::ui_amount_to_amount; use std::str::FromStr; use tokio::time::Instant; use tracing::{error, info}; use crate::jito::{self, get_tip_account, get_tip_value, wait_for_bundle_confirmation}; // prioritization fee = UNIT_PRICE * UNIT_LIMIT fn get_unit_price() -> u64 { env::var("UNIT_PRICE") .ok() .and_then(|v| u64::from_str(&v).ok()) .unwrap_or(20000) } fn get_unit_limit() -> u32 { env::var("UNIT_LIMIT") .ok() .and_then(|v| u32::from_str(&v).ok()) .unwrap_or(200_000) } pub async fn new_signed_and_send( client: &RpcClient, keypair: &Keypair, mut instructions: Vec, use_jito: bool, ) -> Result> { let unit_limit = get_unit_limit(); let unit_price = get_unit_price(); // If not using Jito, manually set the compute unit price and limit if !use_jito { let modify_compute_units = solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit( unit_limit, ); let add_priority_fee = solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price( unit_price, ); instructions.insert(0, modify_compute_units); instructions.insert(1, add_priority_fee); } // send init tx let recent_blockhash = client.get_latest_blockhash()?; let txn = Transaction::new_signed_with_payer( &instructions, Some(&keypair.pubkey()), &vec![&*keypair], recent_blockhash, ); if env::var("TX_SIMULATE").ok() == Some("true".to_string()) { let simulate_result = client.simulate_transaction(&txn)?; if let Some(logs) = simulate_result.value.logs { for log in logs { info!("{}", log); } } return match simulate_result.value.err { Some(err) => Err(anyhow!("{}", err)), None => Ok(vec![]), }; } let start_time = Instant::now(); let mut txs = vec![]; if use_jito { // jito let tip_account = get_tip_account().await?; // jito tip, the upper limit is 0.1 let mut tip = get_tip_value().await?; tip = tip.min(0.1); let tip_lamports = ui_amount_to_amount(tip, spl_token::native_mint::DECIMALS); info!( "tip account: {}, tip(sol): {}, lamports: {}", tip_account, tip, tip_lamports ); let jito_client = Arc::new(JitoRpcClient::new(format!( "{}/api/v1/bundles", jito::BLOCK_ENGINE_URL.to_string() ))); // tip tx let mut bundle: Vec = vec![]; bundle.push(VersionedTransaction::from(txn)); bundle.push(VersionedTransaction::from(system_transaction::transfer( &keypair, &tip_account, tip_lamports, recent_blockhash, ))); let bundle_id = jito_client.send_bundle(&bundle).await?; info!("bundle_id: {}", bundle_id); txs = wait_for_bundle_confirmation( move |id: String| { let client = Arc::clone(&jito_client); async move { let response = client.get_bundle_statuses(&[id]).await; let statuses = response.inspect_err(|err| { error!("Error fetching bundle status: {:?}", err); })?; Ok(statuses.value) } }, bundle_id, Duration::from_millis(1000), Duration::from_secs(10), ) .await?; } else { let sig = common::rpc::send_txn(&client, &txn, true)?; info!("signature: {:?}", sig); txs.push(sig.to_string()); } info!("tx elapsed: {:?}", start_time.elapsed()); Ok(txs) }