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