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