├── .gitignore ├── .env.example ├── src ├── amm │ ├── mod.rs │ ├── amm_info.rs │ └── executor.rs ├── lib.rs ├── api_v3 │ ├── response │ │ ├── pools │ │ │ ├── mod.rs │ │ │ ├── cpmm.rs │ │ │ ├── clmm.rs │ │ │ ├── standard.rs │ │ │ └── base.rs │ │ ├── mod.rs │ │ └── token.rs │ ├── serde_helpers.rs │ ├── client.rs │ └── mod.rs ├── utils.rs ├── types.rs └── builder.rs ├── README.md ├── LICENSE ├── Cargo.toml └── examples └── quote.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | RPC_URL= 2 | RUST_LOG=info -------------------------------------------------------------------------------- /src/amm/mod.rs: -------------------------------------------------------------------------------- 1 | mod amm_info; 2 | pub mod executor; 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod amm; 2 | pub mod api_v3; 3 | pub mod builder; 4 | pub mod types; 5 | pub mod utils; 6 | -------------------------------------------------------------------------------- /src/api_v3/response/pools/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod base; 2 | pub mod clmm; 3 | pub mod cpmm; 4 | pub mod standard; 5 | 6 | pub use base::{ApiV3BasePool, ApiV3BasePoolKeys}; 7 | pub use clmm::{_ApiV3ClmmPool, _ApiV3ClmmPoolKeys}; 8 | pub use standard::{_ApiV3StandardPool, _ApiV3StandardPoolKeys}; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Raydium-Swap 2 | Unofficial library for interacting with the Raydium amm-v3 on-chain contract 3 | 4 | ## DISCLAIMER 5 | This is intended for educational purposes only. Please do not use with real funds unless you accept full responsibility for whatever outcome. This library requires a bit of understanding of Rust and Solana to use correctly 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ademola 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/api_v3/response/pools/cpmm.rs: -------------------------------------------------------------------------------- 1 | use crate::api_v3::response::token::ApiV3Token; 2 | use crate::api_v3::serde_helpers::field_as_string; 3 | use crate::api_v3::PoolType; 4 | 5 | use serde::Deserialize; 6 | use solana_sdk::pubkey::Pubkey; 7 | 8 | #[derive(Clone, Debug, Deserialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct _ApiV3CpmmPool { 11 | /// Standard 12 | #[serde(rename = "type")] 13 | pub pool_type: PoolType, 14 | pub lp_mint: ApiV3Token, 15 | pub lp_price: f64, 16 | pub lp_amount: u64, 17 | pub config: ApiV3CpmmConfig, 18 | } 19 | 20 | #[derive(Clone, Debug, Deserialize)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct ApiV3CpmmConfig { 23 | #[serde(with = "field_as_string")] 24 | pub id: Pubkey, 25 | pub index: u16, 26 | pub protocol_fee_rate: u32, 27 | pub trade_fee_rate: u32, 28 | pub fund_fee_rate: u32, 29 | pub create_pool_fee: String, 30 | } 31 | 32 | #[derive(Clone, Debug, Deserialize)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct _ApiV3CpmmPoolKeys { 35 | #[serde(with = "field_as_string")] 36 | pub authority: Pubkey, 37 | pub mint_lp: ApiV3Token, 38 | pub config: ApiV3CpmmConfig, 39 | } 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "swap" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anchor-client = "0.29.0" 10 | anyhow = "1.0.75" 11 | arrayref = { version = "0.3.6" } 12 | bincode = "1.3.3" 13 | bytemuck = "1.16.1" 14 | clap = { version = "4.4.11", features = ["derive"] } 15 | dotenv = "0.15.0" 16 | env_logger = "0.11.1" 17 | futures-util = "0.3.30" 18 | log = "0.4.20" 19 | once_cell = "1" 20 | rand = "0.8.5" 21 | raydium_amm = { git = "https://github.com/reactive-biscuit/raydium-amm.git", branch = "patch/solana-v1.18.16", default-features = false, features = [ 22 | "client", 23 | ] } 24 | raydium-library = { git = "https://github.com/reactive-biscuit/raydium-library.git", branch = "u128-fix" } 25 | reqwest = { version = "0.11.22", features = ["json"] } 26 | safe-transmute = "0.11.3" 27 | serde = { version = "1.0.193", features = ["derive"] } 28 | serde_json = "1.0.108" 29 | solana-account-decoder = "1.18.16" 30 | solana-client = "1.18.16" 31 | solana-program = "1.18.16" 32 | solana-sdk = "1.18.16" 33 | spl-associated-token-account = { version = "2.3.0", features = ["no-entrypoint"]} 34 | spl-token = "3.2" 35 | tokio = "1.35.0" 36 | -------------------------------------------------------------------------------- /src/api_v3/response/pools/clmm.rs: -------------------------------------------------------------------------------- 1 | use crate::api_v3::response::token::ApiV3Token; 2 | use crate::api_v3::serde_helpers::field_as_string; 3 | use crate::api_v3::PoolType; 4 | 5 | use serde::Deserialize; 6 | use solana_sdk::pubkey::Pubkey; 7 | 8 | #[derive(Clone, Debug, Deserialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct _ApiV3ClmmPool { 11 | /// Concentrated 12 | #[serde(rename = "type")] 13 | pub pool_type: PoolType, 14 | pub config: ApiV3ClmmConfig, 15 | } 16 | 17 | #[derive(Clone, Debug, Deserialize)] 18 | #[serde(rename_all = "camelCase")] 19 | pub struct ApiV3ClmmConfig { 20 | #[serde(with = "field_as_string")] 21 | pub id: Pubkey, 22 | pub index: u16, 23 | pub protocol_fee_rate: u32, 24 | pub trade_fee_rate: u32, 25 | pub tick_spacing: u16, 26 | pub fund_fee_rate: u32, 27 | //description: Option, 28 | pub default_range: f64, 29 | pub default_range_point: Vec, 30 | } 31 | 32 | #[derive(Clone, Debug, Deserialize)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct _ApiV3ClmmPoolKeys { 35 | pub config: ApiV3ClmmConfig, 36 | pub reward_infos: Vec, 37 | } 38 | 39 | #[derive(Clone, Debug, Deserialize)] 40 | pub struct ClmmRewardType { 41 | pub mint: ApiV3Token, 42 | #[serde(with = "field_as_string")] 43 | pub vault: Pubkey, 44 | } 45 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use futures_util::stream::FuturesOrdered; 2 | use futures_util::StreamExt; 3 | use solana_client::nonblocking::rpc_client::RpcClient; 4 | use solana_client::rpc_config::RpcAccountInfoConfig; 5 | use solana_sdk::account::Account; 6 | use solana_sdk::commitment_config::CommitmentConfig; 7 | use solana_sdk::pubkey::Pubkey; 8 | 9 | pub async fn get_multiple_account_data( 10 | rpc_client: &RpcClient, 11 | keys: &[Pubkey], 12 | ) -> anyhow::Result>> { 13 | let mut tasks = FuturesOrdered::new(); 14 | let mut accounts_vec = Vec::with_capacity(keys.len()); 15 | for chunk in keys.chunks(100) { 16 | tasks.push_back(async { 17 | let response = rpc_client 18 | .get_multiple_accounts_with_config( 19 | chunk, 20 | RpcAccountInfoConfig { 21 | encoding: Some(solana_account_decoder::UiAccountEncoding::Base64), 22 | data_slice: None, 23 | commitment: Some(CommitmentConfig::confirmed()), 24 | min_context_slot: None, 25 | }, 26 | ) 27 | .await?; 28 | Ok::<_, anyhow::Error>(response.value) 29 | }); 30 | } 31 | 32 | while let Some(result) = tasks.next().await { 33 | accounts_vec.extend(result?); 34 | } 35 | Ok(accounts_vec) 36 | } 37 | -------------------------------------------------------------------------------- /src/api_v3/response/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pools; 2 | pub mod token; 3 | 4 | use pools::{ 5 | ApiV3BasePool, ApiV3BasePoolKeys, _ApiV3ClmmPool, _ApiV3ClmmPoolKeys, _ApiV3StandardPool, 6 | _ApiV3StandardPoolKeys, 7 | }; 8 | use serde::{Deserialize, Serialize}; 9 | pub use token::ApiV3Token; 10 | 11 | #[derive(Clone, Debug, Serialize, Deserialize)] 12 | pub struct ApiV3Response { 13 | pub id: String, 14 | pub success: bool, 15 | pub data: T, 16 | } 17 | 18 | #[derive(Clone, Debug, Serialize, Deserialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct ApiV3TokenList { 21 | #[serde(default)] 22 | pub mint_list: Vec, 23 | #[serde(default)] 24 | pub blacklist: Vec, 25 | #[serde(default)] 26 | pub whitelist: Vec, 27 | } 28 | 29 | #[derive(Clone, Debug, Serialize, Deserialize)] 30 | #[serde(rename_all = "camelCase")] 31 | pub struct ApiV3PoolsPage { 32 | pub count: u64, 33 | pub has_next_page: bool, 34 | #[serde(rename = "data")] 35 | pub pools: Vec, // 36 | } 37 | 38 | pub type ApiV3StandardPool = ApiV3BasePool<_ApiV3StandardPool>; 39 | pub type ApiV3StandardPoolKeys = ApiV3BasePoolKeys<_ApiV3StandardPoolKeys>; 40 | pub type ApiV3StandardPoolsPage = ApiV3PoolsPage; 41 | 42 | pub type ApiV3ClmmPool = ApiV3BasePool<_ApiV3ClmmPool>; 43 | pub type ApiV3ClmmPoolKeys = ApiV3BasePoolKeys<_ApiV3ClmmPoolKeys>; 44 | pub type ApiV3ClmmPoolsPage = ApiV3PoolsPage; 45 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey::Pubkey; 2 | 3 | #[derive(Copy, Clone, Debug, Default)] 4 | pub enum ComputeUnitLimits { 5 | #[default] 6 | Dynamic, 7 | Fixed(u64), 8 | } 9 | 10 | #[derive(Copy, Clone, Debug)] 11 | pub enum PriorityFeeConfig { 12 | DynamicMultiplier(u64), 13 | FixedCuPrice(u64), 14 | JitoTip(u64), 15 | } 16 | 17 | #[derive(Copy, Clone, Debug)] 18 | pub struct SwapConfig { 19 | pub priority_fee: Option, 20 | pub cu_limits: Option, 21 | pub wrap_and_unwrap_sol: Option, 22 | pub as_legacy_transaction: Option, 23 | } 24 | 25 | #[derive(Clone, Debug, Default)] 26 | pub struct SwapConfigOverrides { 27 | pub priority_fee: Option, 28 | pub cu_limits: Option, 29 | pub wrap_and_unwrap_sol: Option, 30 | pub destination_token_account: Option, 31 | pub as_legacy_transaction: Option, 32 | } 33 | 34 | #[derive(Copy, Clone, Debug)] 35 | pub struct SwapInput { 36 | pub input_token_mint: Pubkey, 37 | pub output_token_mint: Pubkey, 38 | pub slippage_bps: u16, 39 | pub amount: u64, 40 | pub mode: SwapExecutionMode, 41 | pub market: Option, 42 | } 43 | 44 | #[derive(Copy, Clone, Debug)] 45 | pub enum SwapExecutionMode { 46 | ExactIn, 47 | ExactOut, 48 | } 49 | impl SwapExecutionMode { 50 | pub fn amount_specified_is_input(&self) -> bool { 51 | matches!(self, SwapExecutionMode::ExactIn) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/quote.rs: -------------------------------------------------------------------------------- 1 | use solana_client::nonblocking::rpc_client::RpcClient; 2 | use solana_sdk::signature::Keypair; 3 | use solana_sdk::signer::Signer; 4 | use solana_sdk::transaction::VersionedTransaction; 5 | use solana_sdk::{pubkey, pubkey::Pubkey}; 6 | use std::sync::Arc; 7 | use swap::amm::executor::{RaydiumAmm, RaydiumAmmExecutorOpts}; 8 | use swap::api_v3::ApiV3Client; 9 | use swap::types::{SwapExecutionMode, SwapInput}; 10 | 11 | const USDC: Pubkey = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); 12 | const SOL: Pubkey = pubkey!("So11111111111111111111111111111111111111112"); 13 | 14 | #[tokio::main] 15 | pub async fn main() -> anyhow::Result<()> { 16 | dotenv::dotenv()?; 17 | env_logger::init(); 18 | 19 | let client = Arc::new(RpcClient::new(std::env::var("RPC_URL")?)); 20 | let executor = RaydiumAmm::new( 21 | Arc::clone(&client), 22 | RaydiumAmmExecutorOpts::default(), 23 | ApiV3Client::new(None), 24 | ); 25 | let swap_input = SwapInput { 26 | input_token_mint: SOL, 27 | output_token_mint: USDC, 28 | slippage_bps: 1000, // 10% 29 | amount: 1_000_000_000, // 1 SOL 30 | mode: SwapExecutionMode::ExactIn, 31 | market: None, 32 | }; 33 | 34 | let quote = executor.quote(&swap_input).await?; 35 | log::info!("Quote: {:#?}", quote); 36 | 37 | let keypair = Keypair::new(); 38 | let mut transaction = executor 39 | .swap_transaction(keypair.pubkey(), quote, None) 40 | .await?; 41 | let blockhash = client.get_latest_blockhash().await?; 42 | transaction.message.set_recent_blockhash(blockhash); 43 | let _final_tx = VersionedTransaction::try_new(transaction.message, &[&keypair])?; 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /src/api_v3/serde_helpers.rs: -------------------------------------------------------------------------------- 1 | use serde::de::{Deserialize, Deserializer, Error}; 2 | use serde::{Serialize, Serializer}; 3 | use std::str::FromStr; 4 | 5 | pub mod field_as_string { 6 | use super::*; 7 | pub fn serialize(t: &T, serializer: S) -> Result 8 | where 9 | T: ToString, 10 | S: Serializer, 11 | { 12 | t.to_string().serialize(serializer) 13 | } 14 | 15 | pub fn deserialize<'de, T, D>(deserializer: D) -> Result 16 | where 17 | T: FromStr, 18 | D: Deserializer<'de>, 19 | ::Err: std::fmt::Debug, 20 | { 21 | let s: String = String::deserialize(deserializer)?; 22 | s.parse() 23 | .map_err(|e| Error::custom(format!("Parse error: {:?}", e))) 24 | } 25 | } 26 | 27 | pub mod option_field_as_string { 28 | use super::*; 29 | pub fn serialize(t: &Option, serializer: S) -> Result 30 | where 31 | T: ToString, 32 | S: Serializer, 33 | { 34 | if let Some(t) = t { 35 | t.to_string().serialize(serializer) 36 | } else { 37 | serializer.serialize_none() 38 | } 39 | } 40 | 41 | pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> 42 | where 43 | T: FromStr, 44 | D: Deserializer<'de>, 45 | ::Err: std::fmt::Debug, 46 | { 47 | let t: Option = Option::deserialize(deserializer)?; 48 | 49 | match t { 50 | Some(s) => T::from_str(&s) 51 | .map(Some) 52 | .map_err(|_| Error::custom(format!("Parse error for {}", s))), 53 | None => Ok(None), 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/api_v3/response/pools/standard.rs: -------------------------------------------------------------------------------- 1 | use crate::api_v3::response::token::ApiV3Token; 2 | use crate::api_v3::serde_helpers::{field_as_string, option_field_as_string}; 3 | use crate::api_v3::PoolType; 4 | 5 | use serde::Deserialize; 6 | use solana_sdk::pubkey::Pubkey; 7 | 8 | #[derive(Clone, Debug, Deserialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct _ApiV3StandardPool { 11 | /// Standard 12 | #[serde(rename = "type")] 13 | pub pool_type: PoolType, 14 | #[serde(default, with = "option_field_as_string")] 15 | pub market_id: Option, 16 | #[serde(default, with = "option_field_as_string")] 17 | pub config_id: Option, 18 | pub lp_price: f64, 19 | pub lp_amount: f64, 20 | pub lp_mint: ApiV3Token, 21 | } 22 | 23 | #[derive(Clone, Debug, Deserialize)] 24 | #[serde(rename_all = "camelCase")] 25 | pub struct _ApiV3StandardPoolKeys { 26 | #[serde(with = "field_as_string")] 27 | pub authority: Pubkey, 28 | pub mint_lp: ApiV3Token, 29 | #[serde(flatten)] 30 | pub market: Option, 31 | #[serde(default, with = "option_field_as_string")] 32 | pub open_orders: Option, 33 | #[serde(default, with = "option_field_as_string")] 34 | pub target_orders: Option, 35 | } 36 | 37 | #[derive(Clone, Debug, Deserialize)] 38 | #[serde(rename_all = "camelCase")] 39 | pub struct MarketKeys { 40 | #[serde(with = "field_as_string")] 41 | pub market_program_id: Pubkey, 42 | #[serde(with = "field_as_string")] 43 | pub market_id: Pubkey, 44 | #[serde(with = "field_as_string")] 45 | pub market_authority: Pubkey, 46 | #[serde(with = "field_as_string")] 47 | pub market_base_vault: Pubkey, 48 | #[serde(with = "field_as_string")] 49 | pub market_quote_vault: Pubkey, 50 | #[serde(with = "field_as_string")] 51 | pub market_bids: Pubkey, 52 | #[serde(with = "field_as_string")] 53 | pub market_asks: Pubkey, 54 | #[serde(with = "field_as_string")] 55 | pub market_event_queue: Pubkey, 56 | } 57 | -------------------------------------------------------------------------------- /src/api_v3/response/pools/base.rs: -------------------------------------------------------------------------------- 1 | use crate::api_v3::response::token::ApiV3Token; 2 | use crate::api_v3::serde_helpers::{field_as_string, option_field_as_string}; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use solana_sdk::pubkey::Pubkey; 6 | 7 | #[derive(Clone, Debug, Deserialize, Serialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct ApiV3BasePool { 10 | #[serde(with = "field_as_string")] 11 | pub program_id: Pubkey, 12 | #[serde(with = "field_as_string")] 13 | pub id: Pubkey, 14 | pub mint_a: ApiV3Token, 15 | pub mint_b: ApiV3Token, 16 | pub reward_default_infos: Vec, 17 | pub reward_default_pool_infos: Option, // "Ecosystem" | "Fusion" | "Raydium" | "Clmm"; 18 | pub price: f64, 19 | pub mint_amount_a: f64, 20 | pub mint_amount_b: f64, 21 | pub fee_rate: f64, 22 | pub open_time: String, 23 | pub pooltype: Vec, 24 | pub tvl: f64, 25 | pub day: ApiV3PoolInfoCountItem, 26 | pub week: ApiV3PoolInfoCountItem, 27 | pub month: ApiV3PoolInfoCountItem, 28 | pub farm_upcoming_count: u32, 29 | pub farm_ongoing_count: u32, 30 | pub farm_finished_count: u32, 31 | #[serde(flatten)] 32 | pub pool: T, 33 | } 34 | 35 | #[derive(Clone, Debug, Deserialize, Serialize)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct ApiV3BasePoolKeys { 38 | #[serde(with = "field_as_string")] 39 | pub program_id: Pubkey, 40 | #[serde(with = "field_as_string")] 41 | pub id: Pubkey, 42 | pub mint_a: ApiV3Token, 43 | pub mint_b: ApiV3Token, 44 | #[serde(default, with = "option_field_as_string")] 45 | pub lookup_table_account: Option, 46 | pub open_time: String, 47 | pub vault: VaultKeys, 48 | #[serde(flatten)] 49 | pub keys: T, 50 | } 51 | 52 | #[derive(Clone, Debug, Deserialize, Serialize)] 53 | pub struct VaultKeys { 54 | #[serde(rename = "A", with = "field_as_string")] 55 | pub a: Pubkey, 56 | #[serde(rename = "B", with = "field_as_string")] 57 | pub b: Pubkey, 58 | } 59 | 60 | #[derive(Clone, Debug, Deserialize, Serialize)] 61 | #[serde(rename_all = "camelCase")] 62 | pub struct ApiV3PoolFarmRewardInfo { 63 | pub mint: ApiV3Token, 64 | #[serde(with = "field_as_string")] 65 | pub per_second: i64, 66 | #[serde(default, with = "option_field_as_string")] 67 | pub start_time: Option, 68 | #[serde(default, with = "option_field_as_string")] 69 | pub end_time: Option, 70 | } 71 | 72 | #[derive(Clone, Debug, Deserialize, Serialize)] 73 | #[serde(rename_all = "camelCase")] 74 | pub struct ApiV3PoolInfoCountItem { 75 | pub volume: f64, 76 | pub volume_quote: f64, 77 | pub volume_fee: f64, 78 | pub apr: f64, 79 | pub fee_apr: f64, 80 | pub price_min: f64, 81 | pub price_max: f64, 82 | pub reward_apr: Vec, 83 | } 84 | -------------------------------------------------------------------------------- /src/api_v3/response/token.rs: -------------------------------------------------------------------------------- 1 | use crate::api_v3::serde_helpers::field_as_string; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use solana_sdk::pubkey::Pubkey; 5 | 6 | #[derive(Clone, Debug, Serialize, Deserialize)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct ApiV3TokenList { 9 | #[serde(default)] 10 | pub mint_list: Vec, 11 | #[serde(default)] 12 | pub blacklist: Vec, 13 | #[serde(default)] 14 | pub whitelist: Vec, 15 | } 16 | 17 | #[derive(Clone, Debug, Serialize, Deserialize)] 18 | #[serde(rename_all = "camelCase")] 19 | pub struct ApiV3Token { 20 | pub chain_id: u64, 21 | #[serde(with = "field_as_string")] 22 | pub address: Pubkey, 23 | #[serde(default, with = "field_as_string")] 24 | pub program_id: Pubkey, 25 | #[serde(default, rename = "logoURI")] 26 | pub logo_uri: String, 27 | #[serde(default)] 28 | pub symbol: String, 29 | #[serde(default)] 30 | pub name: String, 31 | pub decimals: u8, 32 | #[serde(default)] 33 | pub tags: Vec, 34 | pub extensions: ExtensionsItem, 35 | } 36 | 37 | #[derive(Clone, Debug, Serialize, Deserialize)] 38 | #[serde(rename_all = "camelCase")] 39 | pub struct ExtensionsItem { 40 | pub coingecko_id: Option, 41 | pub fee_config: Option, 42 | } 43 | #[derive(Clone, Debug, Serialize, Deserialize)] 44 | #[serde(rename_all = "camelCase")] 45 | pub struct TransferFeeDatabaseType { 46 | #[serde(with = "field_as_string")] 47 | pub transfer_fee_config_authority: Pubkey, 48 | #[serde(with = "field_as_string")] 49 | pub withdraw_withheld_authority: Pubkey, 50 | pub withheld_amount: String, 51 | pub older_transfer_fee: TransferFee, 52 | pub newer_transfer_fee: TransferFee, 53 | } 54 | 55 | #[derive(Clone, Debug, Serialize, Deserialize)] 56 | #[serde(rename_all = "camelCase")] 57 | pub struct TransferFee { 58 | #[serde(with = "field_as_string")] 59 | pub epoch: u64, 60 | #[serde(with = "field_as_string")] 61 | pub maximum_fee: u64, 62 | pub transfer_fee_basis_points: u16, 63 | } 64 | 65 | #[derive(Clone, Debug, Serialize, Deserialize)] 66 | #[serde(rename_all = "kebab-case")] 67 | pub enum ApiV3TokenTag { 68 | #[serde(rename = "hasFreeze")] 69 | HasFreeze, 70 | #[serde(rename = "hasTransferFee")] 71 | HasTransferFee, 72 | #[serde(rename = "token-2022")] 73 | Token2022, 74 | #[serde(rename = "community")] 75 | Community, 76 | #[serde(rename = "unknown")] 77 | Unknown, 78 | #[serde(untagged)] 79 | UnrecognizedTag(String), 80 | } 81 | 82 | impl std::str::FromStr for ApiV3TokenTag { 83 | type Err = anyhow::Error; 84 | 85 | fn from_str(s: &str) -> Result { 86 | match s { 87 | "hasFreeze" => Ok(Self::HasFreeze), 88 | "hasTransferFee" => Ok(Self::HasTransferFee), 89 | "token-2022" => Ok(Self::Token2022), 90 | "community" => Ok(Self::Community), 91 | "unknown" => Ok(Self::Unknown), 92 | x => Ok(Self::UnrecognizedTag(x.to_string())), 93 | } 94 | } 95 | } 96 | 97 | impl std::fmt::Display for ApiV3TokenTag { 98 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 99 | match self { 100 | Self::HasFreeze => f.write_str("hasFreeze"), 101 | Self::HasTransferFee => f.write_str("hasTransferFee"), 102 | Self::Token2022 => f.write_str("token-2022"), 103 | Self::Community => f.write_str("community"), 104 | Self::Unknown => f.write_str("unknown"), 105 | Self::UnrecognizedTag(x) => f.write_fmt(format_args!("{}", x)), 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/api_v3/client.rs: -------------------------------------------------------------------------------- 1 | use super::response::{ApiV3PoolsPage, ApiV3Token, ApiV3TokenList}; 2 | use super::{handle_response_or_error, PoolFetchParams}; 3 | use serde::de::DeserializeOwned; 4 | use solana_sdk::pubkey::Pubkey; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct ApiV3Client { 8 | base_url: String, 9 | } 10 | 11 | impl Default for ApiV3Client { 12 | fn default() -> Self { 13 | ApiV3Client { 14 | base_url: Self::DEFAULT_BASE_URL.to_string(), 15 | } 16 | } 17 | } 18 | 19 | impl ApiV3Client { 20 | const DEFAULT_BASE_URL: &'static str = "https://api-v3.raydium.io"; 21 | 22 | pub fn new(base_url: Option) -> Self { 23 | ApiV3Client { 24 | base_url: base_url.unwrap_or(Self::DEFAULT_BASE_URL.to_string()), 25 | } 26 | } 27 | 28 | pub async fn get_token_list(&self) -> Result { 29 | let url = format!("{}/mint/list", &self.base_url); 30 | Ok(handle_response_or_error(reqwest::get(url).await?) 31 | .await? 32 | .data) 33 | } 34 | 35 | pub async fn get_jup_token_list(&self) -> Result, anyhow::Error> { 36 | Ok( 37 | reqwest::get("https://tokens.jup.ag/tokens?tags=lst,community") 38 | .await? 39 | .json() 40 | .await?, 41 | ) 42 | } 43 | 44 | pub async fn get_token_info( 45 | &self, 46 | mints: Vec, 47 | ) -> Result, anyhow::Error> { 48 | let mints = mints.join(","); 49 | let url = format!("{}/mint/ids?mints={}", &self.base_url, mints); 50 | Ok(handle_response_or_error(reqwest::get(url).await?) 51 | .await? 52 | .data) 53 | } 54 | 55 | pub async fn get_pool_list( 56 | &self, 57 | params: &PoolFetchParams, 58 | ) -> Result, anyhow::Error> { 59 | let url = format!( 60 | "{}/pools/info/list?poolType={}&poolSortField={}&sortType={}&page={}&pageSize={}", 61 | &self.base_url, 62 | params.pool_type, 63 | params.pool_sort, 64 | params.sort_type, 65 | params.page, 66 | params.page_size 67 | ); 68 | Ok(handle_response_or_error(reqwest::get(url).await?) 69 | .await? 70 | .data) 71 | } 72 | 73 | pub async fn fetch_pools_by_ids( 74 | &self, 75 | ids: Vec, 76 | ) -> Result, anyhow::Error> { 77 | let ids = ids.join(","); 78 | let url = format!("{}/pools/info/ids?ids={}", &self.base_url, ids); 79 | Ok(handle_response_or_error(reqwest::get(url).await?) 80 | .await? 81 | .data) 82 | } 83 | 84 | pub async fn fetch_pool_keys_by_ids( 85 | &self, 86 | ids: Vec, 87 | ) -> Result, anyhow::Error> { 88 | let ids = ids.join(","); 89 | let url = format!("{}/pools/key/ids?ids={}", &self.base_url, ids); 90 | Ok(handle_response_or_error(reqwest::get(url).await?) 91 | .await? 92 | .data) 93 | } 94 | 95 | pub async fn fetch_pool_by_mints( 96 | &self, 97 | mint1: &Pubkey, 98 | mint2: Option<&Pubkey>, 99 | params: &PoolFetchParams, 100 | ) -> Result, anyhow::Error> { 101 | let url = format!( 102 | "{}/pools/info/mint?mint1={}&mint2={}&poolType={}&poolSortField={}&sortType={}&pageSize={}&page={}", 103 | &self.base_url, 104 | mint1, 105 | mint2.map(|x| x.to_string()).unwrap_or_default(), 106 | params.pool_type, 107 | params.pool_sort, 108 | params.sort_type, 109 | 100, 110 | params.page 111 | ); 112 | Ok(handle_response_or_error(reqwest::get(url).await?) 113 | .await? 114 | .data) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/amm/amm_info.rs: -------------------------------------------------------------------------------- 1 | //! Deserialize u128 as u128::from_le_bytes::<[u8; 16]>(..) rather than with unsafe code 2 | 3 | use bytemuck::{Pod, Zeroable}; 4 | use raydium_amm::state::Loadable; 5 | use safe_transmute::trivial::TriviallyTransmutable; 6 | use solana_program::pubkey::Pubkey; 7 | 8 | macro_rules! impl_loadable { 9 | ($type_name:ident) => { 10 | unsafe impl Zeroable for $type_name {} 11 | unsafe impl Pod for $type_name {} 12 | unsafe impl TriviallyTransmutable for $type_name {} 13 | impl Loadable for $type_name {} 14 | }; 15 | } 16 | 17 | #[repr(C)] 18 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 19 | pub struct AmmInfo { 20 | /// Initialized status. 21 | pub status: u64, 22 | /// Nonce used in program address. 23 | /// The program address is created deterministically with the nonce, 24 | /// amm program id, and amm account pubkey. This program address has 25 | /// authority over the amm's token coin account, token pc account, and pool 26 | /// token mint. 27 | pub nonce: u64, 28 | /// max order count 29 | pub order_num: u64, 30 | /// within this range, 5 => 5% range 31 | pub depth: u64, 32 | /// coin decimal 33 | pub coin_decimals: u64, 34 | /// pc decimal 35 | pub pc_decimals: u64, 36 | /// amm machine state 37 | pub state: u64, 38 | /// amm reset_flag 39 | pub reset_flag: u64, 40 | /// min size 1->0.000001 41 | pub min_size: u64, 42 | /// vol_max_cut_ratio numerator, sys_decimal_value as denominator 43 | pub vol_max_cut_ratio: u64, 44 | /// amount wave numerator, sys_decimal_value as denominator 45 | pub amount_wave: u64, 46 | /// coinLotSize 1 -> 0.000001 47 | pub coin_lot_size: u64, 48 | /// pcLotSize 1 -> 0.000001 49 | pub pc_lot_size: u64, 50 | /// min_cur_price: (2 * amm.order_num * amm.pc_lot_size) * max_price_multiplier 51 | pub min_price_multiplier: u64, 52 | /// max_cur_price: (2 * amm.order_num * amm.pc_lot_size) * max_price_multiplier 53 | pub max_price_multiplier: u64, 54 | /// system decimal value, used to normalize the value of coin and pc amount 55 | pub sys_decimal_value: u64, 56 | /// All fee information 57 | pub fees: raydium_amm::state::Fees, 58 | /// Statistical data 59 | pub state_data: StateData, 60 | /// Coin vault 61 | pub coin_vault: Pubkey, 62 | /// Pc vault 63 | pub pc_vault: Pubkey, 64 | /// Coin vault mint 65 | pub coin_vault_mint: Pubkey, 66 | /// Pc vault mint 67 | pub pc_vault_mint: Pubkey, 68 | /// lp mint 69 | pub lp_mint: Pubkey, 70 | /// open_orders key 71 | pub open_orders: Pubkey, 72 | /// market key 73 | pub market: Pubkey, 74 | /// market program key 75 | pub market_program: Pubkey, 76 | /// target_orders key 77 | pub target_orders: Pubkey, 78 | /// padding 79 | pub padding1: [u64; 8], 80 | /// amm owner key 81 | pub amm_owner: Pubkey, 82 | /// pool lp amount 83 | pub lp_amount: u64, 84 | /// client order id 85 | pub client_order_id: u64, 86 | /// padding 87 | pub padding2: [u64; 2], 88 | } 89 | impl_loadable!(AmmInfo); 90 | 91 | #[repr(C)] 92 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 93 | pub struct StateData { 94 | /// delay to take pnl coin 95 | pub need_take_pnl_coin: u64, 96 | /// delay to take pnl pc 97 | pub need_take_pnl_pc: u64, 98 | /// total pnl pc 99 | pub total_pnl_pc: u64, 100 | /// total pnl coin 101 | pub total_pnl_coin: u64, 102 | /// ido pool open time 103 | pub pool_open_time: u64, 104 | /// padding for future updates 105 | pub padding: [u64; 2], 106 | /// switch from orderbookonly to init 107 | pub orderbook_to_init_time: u64, 108 | 109 | /// swap coin in amount 110 | pub swap_coin_in_amount: [u8; 16], 111 | /// swap pc out amount 112 | pub swap_pc_out_amount: [u8; 16], 113 | /// charge pc as swap fee while swap pc to coin 114 | pub swap_acc_pc_fee: u64, 115 | 116 | /// swap pc in amount 117 | pub swap_pc_in_amount: [u8; 16], 118 | /// swap coin out amount 119 | pub swap_coin_out_amount: [u8; 16], 120 | /// charge coin as swap fee while swap coin to pc 121 | pub swap_acc_coin_fee: u64, 122 | } 123 | 124 | impl From for raydium_amm::state::StateData { 125 | fn from(value: StateData) -> Self { 126 | Self { 127 | need_take_pnl_coin: value.need_take_pnl_coin, 128 | need_take_pnl_pc: value.need_take_pnl_pc, 129 | total_pnl_coin: value.total_pnl_coin, 130 | total_pnl_pc: value.total_pnl_pc, 131 | pool_open_time: value.pool_open_time, 132 | padding: value.padding, 133 | orderbook_to_init_time: value.orderbook_to_init_time, 134 | swap_acc_pc_fee: value.swap_acc_pc_fee, 135 | swap_acc_coin_fee: value.swap_acc_coin_fee, 136 | swap_coin_in_amount: u128::from_le_bytes(value.swap_coin_in_amount), 137 | swap_pc_out_amount: u128::from_le_bytes(value.swap_pc_out_amount), 138 | swap_pc_in_amount: u128::from_le_bytes(value.swap_pc_in_amount), 139 | swap_coin_out_amount: u128::from_le_bytes(value.swap_coin_out_amount), 140 | } 141 | } 142 | } 143 | 144 | impl From for raydium_amm::state::AmmInfo { 145 | fn from(value: AmmInfo) -> Self { 146 | raydium_amm::state::AmmInfo { 147 | status: value.status, 148 | nonce: value.nonce, 149 | order_num: value.order_num, 150 | depth: value.depth, 151 | coin_decimals: value.coin_decimals, 152 | pc_decimals: value.pc_decimals, 153 | state: value.state, 154 | reset_flag: value.reset_flag, 155 | min_size: value.min_size, 156 | vol_max_cut_ratio: value.vol_max_cut_ratio, 157 | amount_wave: value.amount_wave, 158 | coin_lot_size: value.coin_lot_size, 159 | pc_lot_size: value.pc_lot_size, 160 | min_price_multiplier: value.min_price_multiplier, 161 | max_price_multiplier: value.max_price_multiplier, 162 | sys_decimal_value: value.sys_decimal_value, 163 | fees: value.fees, 164 | state_data: value.state_data.into(), 165 | coin_vault: value.coin_vault, 166 | pc_vault: value.pc_vault, 167 | coin_vault_mint: value.coin_vault_mint, 168 | pc_vault_mint: value.pc_vault_mint, 169 | lp_mint: value.lp_mint, 170 | open_orders: value.open_orders, 171 | market: value.market, 172 | market_program: value.market_program, 173 | target_orders: value.target_orders, 174 | padding1: value.padding1, 175 | amm_owner: value.amm_owner, 176 | lp_amount: value.lp_amount, 177 | client_order_id: value.client_order_id, 178 | padding2: value.padding2, 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/api_v3/mod.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | pub mod response; 3 | mod serde_helpers; 4 | 5 | use anyhow::Context; 6 | pub use client::ApiV3Client; 7 | use response::ApiV3Response; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | pub type ApiV3Result = Result, anyhow::Error>; 11 | 12 | #[derive(Clone, Debug, Deserialize)] 13 | pub struct ApiV3ErrorResponse { 14 | pub id: String, 15 | pub success: bool, 16 | pub msg: String, 17 | } 18 | 19 | impl std::fmt::Display for ApiV3ErrorResponse { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | f.write_fmt(format_args!( 22 | "Received error response from API: {}", 23 | self.msg 24 | )) 25 | } 26 | } 27 | impl std::error::Error for ApiV3ErrorResponse {} 28 | 29 | async fn handle_response_or_error( 30 | response: reqwest::Response, 31 | ) -> Result, anyhow::Error> 32 | where 33 | T: serde::de::DeserializeOwned, 34 | { 35 | let response = response.error_for_status()?; 36 | let json = response.json::().await?; 37 | let success = json 38 | .get("success") 39 | .and_then(|v| v.as_bool()) 40 | .context("Invalid api response")?; 41 | 42 | if success { 43 | Ok(serde_json::from_value::>(json)?) 44 | } else { 45 | Err(serde_json::from_value::(json)?.into()) 46 | } 47 | } 48 | 49 | #[derive(Clone, Debug, Default)] 50 | pub struct PoolFetchParams { 51 | pub pool_type: PoolType, 52 | pub pool_sort: PoolSort, 53 | pub sort_type: PoolSortOrder, 54 | pub page_size: u16, 55 | pub page: u16, 56 | } 57 | 58 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 59 | #[serde(rename_all = "PascalCase")] 60 | pub enum PoolType { 61 | #[default] 62 | All, 63 | Standard, 64 | Concentrated, 65 | AllFarm, 66 | StandardFarm, 67 | ConcentratedFarm, 68 | } 69 | impl std::fmt::Display for PoolType { 70 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 71 | match self { 72 | PoolType::All => f.write_str("all"), 73 | PoolType::Standard => f.write_str("standard"), 74 | PoolType::Concentrated => f.write_str("concentrated"), 75 | PoolType::AllFarm => f.write_str("allFarm"), 76 | PoolType::StandardFarm => f.write_str("standardFarm"), 77 | PoolType::ConcentratedFarm => f.write_str("concentratedFarm"), 78 | } 79 | } 80 | } 81 | 82 | #[derive(Clone, Debug, Default)] 83 | pub enum PoolSort { 84 | #[default] 85 | Liquidity, 86 | Volume24h, 87 | Volume7d, 88 | Volume30d, 89 | Fee24h, 90 | Fee7d, 91 | Fee30d, 92 | Apr24h, 93 | Apr7d, 94 | Apr30d, 95 | } 96 | impl std::fmt::Display for PoolSort { 97 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 98 | match self { 99 | PoolSort::Liquidity => f.write_str("liquidity"), 100 | PoolSort::Volume24h => f.write_str("volume24h"), 101 | PoolSort::Volume7d => f.write_str("volume7d"), 102 | PoolSort::Volume30d => f.write_str("volume30d"), 103 | PoolSort::Fee24h => f.write_str("fee24h"), 104 | PoolSort::Fee7d => f.write_str("fee7d"), 105 | PoolSort::Fee30d => f.write_str("fee30d"), 106 | PoolSort::Apr24h => f.write_str("apr24h"), 107 | PoolSort::Apr7d => f.write_str("apr7d"), 108 | PoolSort::Apr30d => f.write_str("apr30d"), 109 | } 110 | } 111 | } 112 | 113 | #[derive(Clone, Debug, Default)] 114 | pub enum PoolSortOrder { 115 | Ascending, 116 | #[default] 117 | Descending, 118 | } 119 | impl std::fmt::Display for PoolSortOrder { 120 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 121 | match self { 122 | PoolSortOrder::Ascending => f.write_str("asc"), 123 | PoolSortOrder::Descending => f.write_str("desc"), 124 | } 125 | } 126 | } 127 | 128 | #[cfg(test)] 129 | pub mod raydium_api_v3 { 130 | use super::response::{ 131 | ApiV3ClmmPool, ApiV3ClmmPoolKeys, ApiV3StandardPool, ApiV3StandardPoolKeys, 132 | }; 133 | use super::{ApiV3Client, PoolFetchParams, PoolSort, PoolSortOrder, PoolType}; 134 | 135 | #[tokio::test] 136 | pub async fn get_token_list_and_info() { 137 | let client = ApiV3Client::default(); 138 | let token_list = client.get_token_list().await.unwrap(); 139 | let keys = token_list 140 | .mint_list 141 | .into_iter() 142 | .take(5) 143 | .map(|token| token.address.to_string()) 144 | .collect::>(); 145 | let token_info = client.get_token_info(keys).await.unwrap(); 146 | assert!(token_info.len() == 5); 147 | } 148 | 149 | #[tokio::test] 150 | pub async fn get_pool_list() { 151 | let client = ApiV3Client::default(); 152 | let _pools = client 153 | .get_pool_list::(&Default::default()) 154 | .await 155 | .unwrap(); 156 | } 157 | 158 | #[tokio::test] 159 | pub async fn get_standard_pool_and_keys() { 160 | let client = ApiV3Client::default(); 161 | let params = PoolFetchParams { 162 | pool_type: PoolType::Standard, 163 | pool_sort: PoolSort::Liquidity, 164 | sort_type: PoolSortOrder::Ascending, 165 | page_size: 20, 166 | page: 1, 167 | }; 168 | 169 | let pools = client 170 | .get_pool_list::(¶ms) 171 | .await 172 | .unwrap(); 173 | 174 | let ids = pools 175 | .pools 176 | .iter() 177 | .map(|p| p.id.to_string()) 178 | .collect::>(); 179 | let pools_by_id = client 180 | .fetch_pools_by_ids::(ids.clone()) 181 | .await 182 | .unwrap(); 183 | let pool_keys_by_id = client 184 | .fetch_pool_keys_by_ids::(ids) 185 | .await 186 | .unwrap(); 187 | 188 | for pool in pools.pools.iter().take(3) { 189 | let pools_by_mint = client 190 | .fetch_pool_by_mints::( 191 | &pool.mint_a.address, 192 | Some(&pool.mint_b.address), 193 | ¶ms, 194 | ) 195 | .await 196 | .unwrap(); 197 | let found = pools_by_mint.pools.iter().find(|p| p.id == pool.id); 198 | assert!(found.is_some()); 199 | 200 | let pool_by_id = pools_by_id.iter().find(|p| p.id == pool.id); 201 | assert!(pool_by_id.is_some()); 202 | assert!(pool_by_id.unwrap().id == pool.id); 203 | 204 | let pool_keys_by_id = pool_keys_by_id.iter().find(|p| p.id == pool.id); 205 | assert!(pool_keys_by_id.is_some()); 206 | assert!(pool_keys_by_id.unwrap().id == pool.id); 207 | } 208 | } 209 | 210 | #[tokio::test] 211 | pub async fn get_clmm_pool_and_keys() { 212 | let client = ApiV3Client::default(); 213 | let params = PoolFetchParams { 214 | pool_type: PoolType::Concentrated, 215 | pool_sort: PoolSort::Liquidity, 216 | sort_type: PoolSortOrder::Ascending, 217 | page_size: 100, 218 | page: 1, 219 | }; 220 | let pools = client 221 | .get_pool_list::(¶ms) 222 | .await 223 | .unwrap(); 224 | 225 | let ids = pools 226 | .pools 227 | .iter() 228 | .map(|p| p.id.to_string()) 229 | .collect::>(); 230 | let pools_by_id = client 231 | .fetch_pools_by_ids::(ids.clone()) 232 | .await 233 | .unwrap(); 234 | let pool_keys_by_id = client 235 | .fetch_pool_keys_by_ids::(ids) 236 | .await 237 | .unwrap(); 238 | 239 | for pool in pools.pools.iter().take(3) { 240 | let pools_by_mint = client 241 | .fetch_pool_by_mints::( 242 | &pool.mint_a.address, 243 | Some(&pool.mint_b.address), 244 | ¶ms, 245 | ) 246 | .await 247 | .unwrap(); 248 | let found = pools_by_mint.pools.iter().find(|p| p.id == pool.id); 249 | assert!(found.is_some()); 250 | 251 | let pool_by_id = pools_by_id.iter().find(|p| p.id == pool.id); 252 | assert!(pool_by_id.is_some()); 253 | assert!(pool_by_id.unwrap().id == pool.id); 254 | 255 | let pool_keys_by_id = pool_keys_by_id.iter().find(|p| p.id == pool.id); 256 | assert!(pool_keys_by_id.is_some()); 257 | assert!(pool_keys_by_id.unwrap().id == pool.id); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{ComputeUnitLimits, PriorityFeeConfig}; 2 | use anyhow::Context; 3 | use rand::Rng; 4 | use solana_client::nonblocking::rpc_client::RpcClient; 5 | use solana_client::rpc_config::RpcSimulateTransactionConfig; 6 | use solana_program::message::{Message, VersionedMessage}; 7 | use solana_sdk::commitment_config::CommitmentConfig; 8 | use solana_sdk::hash::Hash; 9 | use solana_sdk::instruction::Instruction; 10 | use solana_sdk::signature::Signature; 11 | use solana_sdk::transaction::VersionedTransaction; 12 | use solana_sdk::{pubkey, pubkey::Pubkey}; 13 | 14 | /// Protocol defined: The default compute units set for a transaction 15 | const DEFAULT_INSTRUCTION_COMPUTE_UNIT: u32 = 200_000; 16 | /// Protocol defined: There are 10^6 micro-lamports in one lamport 17 | const MICRO_LAMPORTS_PER_LAMPORT: u64 = 1_000_000; 18 | 19 | #[derive(Default, Clone)] 20 | pub struct SwapInstructionsBuilder { 21 | pub compute_budget_instructions: Vec, 22 | pub setup_instructions: Vec, 23 | pub swap_instruction: Option, 24 | pub cleanup_instruction: Option, 25 | pub address_lookup_table_addresses: Vec, 26 | } 27 | 28 | pub struct UserAssociatedTokenAccounts { 29 | pub input_ata: Pubkey, 30 | pub output_ata: Pubkey, 31 | } 32 | 33 | impl SwapInstructionsBuilder { 34 | #[allow(clippy::too_many_arguments)] 35 | pub fn handle_token_wrapping_and_accounts_creation( 36 | &mut self, 37 | user: Pubkey, 38 | wrap_and_unwrap_sol: bool, 39 | input_amount: u64, 40 | input_mint: Pubkey, 41 | output_mint: Pubkey, 42 | input_token_program: Pubkey, 43 | output_token_program: Pubkey, 44 | destination_token_account: Option, 45 | ) -> anyhow::Result { 46 | let user_input_ata = 47 | spl_associated_token_account::get_associated_token_address_with_program_id( 48 | &user, 49 | &input_mint, 50 | &input_token_program, 51 | ); 52 | let user_output_ata = 53 | spl_associated_token_account::get_associated_token_address_with_program_id( 54 | &user, 55 | &output_mint, 56 | &output_token_program, 57 | ); 58 | 59 | if input_mint == spl_token::native_mint::ID { 60 | // Only create an input-ata if it's the native mint 61 | let create_ata_ix = 62 | spl_associated_token_account::instruction::create_associated_token_account_idempotent( 63 | &user, 64 | &user, 65 | &input_mint, 66 | &spl_token::ID, // SOL uses token-22 67 | ); 68 | self.setup_instructions.push(create_ata_ix); 69 | 70 | // Only wrap SOL if user specifies this behaviour and the input-token is SOL 71 | if wrap_and_unwrap_sol { 72 | let transfer_ix = 73 | solana_sdk::system_instruction::transfer(&user, &user_input_ata, input_amount); 74 | let sync_ix = spl_token::instruction::sync_native(&spl_token::ID, &user_input_ata) 75 | .expect("spl_token::ID is valid"); 76 | self.setup_instructions.extend([transfer_ix, sync_ix]); 77 | 78 | let close_ix = spl_token::instruction::close_account( 79 | &spl_token::ID, 80 | &user_input_ata, 81 | &user, 82 | &user, 83 | &[], 84 | ) 85 | .expect("spl_token::ID is valid"); 86 | self.cleanup_instruction = Some(close_ix); 87 | } 88 | } 89 | 90 | if destination_token_account.is_none() { 91 | // Only create an ATA if no destination-token-account is specified. If specified, we assume it is 92 | // already initialized. 93 | let create_ata_ix = 94 | spl_associated_token_account::instruction::create_associated_token_account_idempotent( 95 | &user, 96 | &user, 97 | &output_mint, 98 | &output_token_program, 99 | ); 100 | self.setup_instructions.push(create_ata_ix); 101 | 102 | if wrap_and_unwrap_sol && output_mint == spl_token::native_mint::ID { 103 | self.cleanup_instruction = Some( 104 | spl_token::instruction::close_account( 105 | &spl_token::ID, 106 | &user_output_ata, 107 | &user, 108 | &user, 109 | &[], 110 | ) 111 | .expect("spl_token::ID is valid"), 112 | ) 113 | } 114 | } 115 | 116 | Ok(UserAssociatedTokenAccounts { 117 | input_ata: user_input_ata, 118 | output_ata: user_output_ata, 119 | }) 120 | } 121 | 122 | pub fn handle_priority_fee_params( 123 | &mut self, 124 | priority_fee_config: Option, 125 | compute_units: Option, 126 | funder: Pubkey, 127 | ) -> anyhow::Result<()> { 128 | let compute_units = compute_units.unwrap_or(DEFAULT_INSTRUCTION_COMPUTE_UNIT); 129 | log::debug!("Prioritization fee config: {priority_fee_config:#?}"); 130 | match priority_fee_config { 131 | Some(PriorityFeeConfig::FixedCuPrice(cu_price)) => { 132 | log::trace!("setting user defined cu-price: {}", cu_price); 133 | let compute_ix = 134 | solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price( 135 | cu_price, 136 | ); 137 | self.compute_budget_instructions.push(compute_ix); 138 | } 139 | Some(PriorityFeeConfig::DynamicMultiplier(multiplier)) => { 140 | let priofee = multiplier 141 | .checked_mul(100_000) 142 | .context("Overflow error while calculating priofee auto-multiplier")?; 143 | let cu_price = calculate_cu_price(priofee, compute_units); 144 | log::trace!( 145 | "prioritization-fee-lamports: cu-price={}, multiplier={}. priofee={}, cu-limit={}", 146 | cu_price, 147 | multiplier, 148 | priofee, 149 | compute_units 150 | ); 151 | let compute_ix = 152 | solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price( 153 | cu_price, 154 | ); 155 | self.compute_budget_instructions.push(compute_ix); 156 | } 157 | Some(PriorityFeeConfig::JitoTip(jito_tip)) => { 158 | let tip_ix = build_jito_tip_ix(&funder, jito_tip); 159 | self.setup_instructions.push(tip_ix); 160 | } 161 | None => {} 162 | } 163 | 164 | Ok(()) 165 | } 166 | 167 | pub async fn handle_compute_units_params( 168 | &mut self, 169 | compute_limits: Option, 170 | rpc_client: &RpcClient, 171 | payer: Pubkey, 172 | ) -> anyhow::Result> { 173 | let cu_limit = match compute_limits { 174 | None => None, 175 | Some(ComputeUnitLimits::Dynamic) => { 176 | let simulate_txn = self.clone().build_transaction(Some(&payer), None)?; 177 | let result = rpc_client 178 | .simulate_transaction_with_config( 179 | &simulate_txn, 180 | RpcSimulateTransactionConfig { 181 | sig_verify: false, 182 | replace_recent_blockhash: true, 183 | commitment: Some(CommitmentConfig::confirmed()), 184 | ..Default::default() 185 | }, 186 | ) 187 | .await?; 188 | 189 | result.value.units_consumed.and_then(|compute_units| { 190 | u32::try_from(compute_units).ok()?.checked_add(50_000) 191 | }) 192 | } 193 | Some(ComputeUnitLimits::Fixed(cu_limits)) => Some(u32::try_from(cu_limits)?), 194 | }; 195 | 196 | if let Some(cu_limit) = cu_limit { 197 | self.compute_budget_instructions.push( 198 | solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit( 199 | cu_limit, 200 | ), 201 | ); 202 | } 203 | 204 | Ok(cu_limit) 205 | } 206 | 207 | pub fn build_instructions(self) -> anyhow::Result> { 208 | let mut final_instructions = Vec::new(); 209 | let SwapInstructionsBuilder { 210 | compute_budget_instructions, 211 | setup_instructions, 212 | swap_instruction, 213 | cleanup_instruction, 214 | address_lookup_table_addresses: _, 215 | } = self; 216 | final_instructions.extend(compute_budget_instructions); 217 | final_instructions.extend(setup_instructions); 218 | final_instructions.push(swap_instruction.context("Swap instruction not set")?); 219 | if let Some(cleanup_instruction) = cleanup_instruction { 220 | final_instructions.push(cleanup_instruction); 221 | } 222 | Ok(final_instructions) 223 | } 224 | 225 | pub fn build_transaction( 226 | self, 227 | payer: Option<&Pubkey>, 228 | blockhash: Option, 229 | ) -> anyhow::Result { 230 | let instructions = self.build_instructions()?; 231 | let mut message = VersionedMessage::Legacy(Message::new(&instructions, payer)); 232 | if let Some(hash) = blockhash { 233 | message.set_recent_blockhash(hash); 234 | } 235 | Ok(VersionedTransaction { 236 | signatures: vec![Signature::default()], 237 | message, 238 | }) 239 | } 240 | } 241 | 242 | fn calculate_cu_price(priority_fee: u64, compute_units: u32) -> u64 { 243 | // protocol: priority-fee = cu-price * cu-limit / 1_000_000 244 | // agave: priority-fee = (cu-price * cu-limit + 999_999) / 1_000_000 245 | let cu_price = (priority_fee as u128) 246 | .checked_mul(MICRO_LAMPORTS_PER_LAMPORT as u128) 247 | .expect("u128 multiplication shouldn't overflow") 248 | .saturating_sub(MICRO_LAMPORTS_PER_LAMPORT as u128 - 1) 249 | .checked_div(compute_units as u128 + 1) 250 | .expect("non-zero compute units"); 251 | log::trace!("cu-price u128: {}", cu_price); 252 | u64::try_from(cu_price).unwrap_or(u64::MAX) 253 | } 254 | 255 | const JITO_TIP_ACCOUNTS: [Pubkey; 8] = [ 256 | pubkey!("96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5"), 257 | pubkey!("HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe"), 258 | pubkey!("Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY"), 259 | pubkey!("ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49"), 260 | pubkey!("DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh"), 261 | pubkey!("ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt"), 262 | pubkey!("DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL"), 263 | pubkey!("3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT"), 264 | ]; 265 | 266 | fn build_jito_tip_ix(from: &Pubkey, tip: u64) -> Instruction { 267 | let random_recipient = 268 | &JITO_TIP_ACCOUNTS[rand::thread_rng().gen_range(0..JITO_TIP_ACCOUNTS.len())]; 269 | solana_sdk::system_instruction::transfer(from, random_recipient, tip) 270 | } 271 | -------------------------------------------------------------------------------- /src/amm/executor.rs: -------------------------------------------------------------------------------- 1 | use crate::api_v3::response::{ApiV3PoolsPage, ApiV3StandardPool, ApiV3StandardPoolKeys}; 2 | use crate::api_v3::{ApiV3Client, PoolFetchParams, PoolSort, PoolSortOrder, PoolType}; 3 | use crate::builder::SwapInstructionsBuilder; 4 | use crate::types::{ 5 | ComputeUnitLimits, PriorityFeeConfig, SwapConfig, SwapConfigOverrides, SwapInput, 6 | }; 7 | use std::sync::Arc; 8 | 9 | use anyhow::{anyhow, Context}; 10 | use arrayref::array_ref; 11 | use raydium_library::amm::AmmKeys; 12 | use safe_transmute::{transmute_one_pedantic, transmute_to_bytes}; 13 | use solana_client::nonblocking::rpc_client::RpcClient; 14 | use solana_sdk::account_info::IntoAccountInfo; 15 | use solana_sdk::instruction::Instruction; 16 | use solana_sdk::program_pack::Pack; 17 | use solana_sdk::transaction::VersionedTransaction; 18 | use solana_sdk::{pubkey, pubkey::Pubkey}; 19 | 20 | const RAYDIUM_LIQUIDITY_POOL_V4_PROGRAM_ID: Pubkey = 21 | pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"); 22 | // // https://api-v3.raydium.io/pools/info/mint?mint1=So11111111111111111111111111111111111111112&mint2=EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm&poolType=standard&poolSortField=liquidity&sortType=desc&pageSize=100&page=1 23 | 24 | #[derive(Clone)] 25 | pub struct RaydiumAmm { 26 | client: Arc, 27 | api: ApiV3Client, 28 | config: SwapConfig, 29 | load_keys_by_api: bool, 30 | } 31 | 32 | // todo: Builder pattern for this 33 | #[derive(Default)] 34 | pub struct RaydiumAmmExecutorOpts { 35 | pub priority_fee: Option, 36 | pub cu_limits: Option, 37 | pub wrap_and_unwrap_sol: Option, 38 | pub load_keys_by_api: Option, 39 | } 40 | 41 | impl RaydiumAmm { 42 | pub fn new(client: Arc, config: RaydiumAmmExecutorOpts, api: ApiV3Client) -> Self { 43 | let RaydiumAmmExecutorOpts { 44 | priority_fee, 45 | cu_limits, 46 | wrap_and_unwrap_sol, 47 | load_keys_by_api, 48 | } = config; 49 | Self { 50 | client, 51 | api, 52 | load_keys_by_api: load_keys_by_api.unwrap_or(true), 53 | config: SwapConfig { 54 | priority_fee, 55 | cu_limits, 56 | wrap_and_unwrap_sol, 57 | as_legacy_transaction: Some(true), 58 | }, 59 | } 60 | } 61 | 62 | pub async fn quote(&self, swap_input: &SwapInput) -> anyhow::Result { 63 | if swap_input.input_token_mint == swap_input.output_token_mint { 64 | return Err(anyhow!( 65 | "Input token cannot equal output token {}", 66 | swap_input.input_token_mint 67 | )); 68 | } 69 | 70 | let mut pool_id = swap_input.market; 71 | if pool_id.is_none() { 72 | let response: ApiV3PoolsPage = self 73 | .api 74 | .fetch_pool_by_mints( 75 | &swap_input.input_token_mint, 76 | Some(&swap_input.output_token_mint), 77 | &PoolFetchParams { 78 | pool_type: PoolType::Standard, 79 | pool_sort: PoolSort::Liquidity, 80 | sort_type: PoolSortOrder::Descending, 81 | page_size: 10, 82 | page: 1, 83 | }, 84 | ) 85 | .await?; 86 | pool_id = response.pools.into_iter().find_map(|pool| { 87 | if pool.mint_a.address == swap_input.input_token_mint 88 | && pool.mint_b.address == swap_input.output_token_mint 89 | || pool.mint_a.address == swap_input.output_token_mint 90 | && pool.mint_b.address == swap_input.input_token_mint 91 | && pool.program_id == RAYDIUM_LIQUIDITY_POOL_V4_PROGRAM_ID 92 | { 93 | Some(pool.id) 94 | } else { 95 | None 96 | } 97 | }); 98 | } 99 | 100 | let Some(pool_id) = pool_id else { 101 | return Err(anyhow!("Failed to get market for swap")); 102 | }; 103 | 104 | let (amm_keys, market_keys) = if self.load_keys_by_api { 105 | let response = self 106 | .api 107 | .fetch_pool_keys_by_ids::( 108 | [&pool_id].into_iter().map(|id| id.to_string()).collect(), 109 | ) 110 | .await?; 111 | let keys = response.first().context(format!( 112 | "Failed to get pool keys for raydium standard pool {}", 113 | pool_id 114 | ))?; 115 | 116 | (AmmKeys::try_from(keys)?, MarketKeys::try_from(keys)?) 117 | } else { 118 | let amm_keys = raydium_library::amm::utils::load_amm_keys( 119 | &self.client, 120 | &RAYDIUM_LIQUIDITY_POOL_V4_PROGRAM_ID, 121 | &pool_id, 122 | ) 123 | .await?; 124 | 125 | let market_keys = MarketKeys::from( 126 | &raydium_library::amm::openbook::get_keys_for_market( 127 | &self.client, 128 | &amm_keys.market_program, 129 | &amm_keys.market, 130 | ) 131 | .await?, 132 | ); 133 | 134 | (amm_keys, market_keys) 135 | }; 136 | 137 | // reload accounts data to calculate amm pool vault amount 138 | // get multiple accounts at the same time to ensure data consistency 139 | let load_pubkeys = vec![ 140 | pool_id, 141 | amm_keys.amm_target, 142 | amm_keys.amm_pc_vault, 143 | amm_keys.amm_coin_vault, 144 | amm_keys.amm_open_order, 145 | amm_keys.market, 146 | market_keys.event_queue, 147 | ]; 148 | let rsps = crate::utils::get_multiple_account_data(&self.client, &load_pubkeys).await?; 149 | let accounts = array_ref![rsps, 0, 7]; 150 | let [amm_account, amm_target_account, amm_pc_vault_account, amm_coin_vault_account, amm_open_orders_account, market_account, market_event_q_account] = 151 | accounts; 152 | let amm: raydium_amm::state::AmmInfo = transmute_one_pedantic::( 153 | transmute_to_bytes(&amm_account.as_ref().unwrap().clone().data), 154 | ) 155 | .map_err(|e| e.without_src())? 156 | .into(); 157 | let _amm_target: raydium_amm::state::TargetOrders = 158 | transmute_one_pedantic::(transmute_to_bytes( 159 | &amm_target_account.as_ref().unwrap().clone().data, 160 | )) 161 | .map_err(|e| e.without_src())?; 162 | let amm_pc_vault = 163 | spl_token::state::Account::unpack(&amm_pc_vault_account.as_ref().unwrap().clone().data) 164 | .unwrap(); 165 | let amm_coin_vault = spl_token::state::Account::unpack( 166 | &amm_coin_vault_account.as_ref().unwrap().clone().data, 167 | ) 168 | .unwrap(); 169 | let (amm_pool_pc_vault_amount, amm_pool_coin_vault_amount) = 170 | if raydium_amm::state::AmmStatus::from_u64(amm.status).orderbook_permission() { 171 | let amm_open_orders_account = 172 | &mut amm_open_orders_account.as_ref().unwrap().clone(); 173 | let market_account = &mut market_account.as_ref().unwrap().clone(); 174 | let market_event_q_account = &mut market_event_q_account.as_ref().unwrap().clone(); 175 | let amm_open_orders_info = 176 | (&amm.open_orders, amm_open_orders_account).into_account_info(); 177 | let market_account_info = (&amm.market, market_account).into_account_info(); 178 | let market_event_queue_info = 179 | (&(market_keys.event_queue), market_event_q_account).into_account_info(); 180 | let amm_authority = Pubkey::find_program_address( 181 | &[raydium_amm::processor::AUTHORITY_AMM], 182 | &RAYDIUM_LIQUIDITY_POOL_V4_PROGRAM_ID, 183 | ) 184 | .0; 185 | let lamports = &mut 0; 186 | let data = &mut [0u8]; 187 | let owner = Pubkey::default(); 188 | let amm_authority_info = solana_program::account_info::AccountInfo::new( 189 | &amm_authority, 190 | false, 191 | false, 192 | lamports, 193 | data, 194 | &owner, 195 | false, 196 | 0, 197 | ); 198 | let (market_state, open_orders) = 199 | raydium_amm::processor::Processor::load_serum_market_order( 200 | &market_account_info, 201 | &amm_open_orders_info, 202 | &amm_authority_info, 203 | &amm, 204 | false, 205 | )?; 206 | let (amm_pool_pc_vault_amount, amm_pool_coin_vault_amount) = 207 | raydium_amm::math::Calculator::calc_total_without_take_pnl( 208 | amm_pc_vault.amount, 209 | amm_coin_vault.amount, 210 | &open_orders, 211 | &amm, 212 | &market_state, 213 | &market_event_queue_info, 214 | &amm_open_orders_info, 215 | )?; 216 | (amm_pool_pc_vault_amount, amm_pool_coin_vault_amount) 217 | } else { 218 | let (amm_pool_pc_vault_amount, amm_pool_coin_vault_amount) = 219 | raydium_amm::math::Calculator::calc_total_without_take_pnl_no_orderbook( 220 | amm_pc_vault.amount, 221 | amm_coin_vault.amount, 222 | &amm, 223 | )?; 224 | (amm_pool_pc_vault_amount, amm_pool_coin_vault_amount) 225 | }; 226 | 227 | let (direction, coin_to_pc) = if swap_input.input_token_mint == amm_keys.amm_coin_mint 228 | && swap_input.output_token_mint == amm_keys.amm_pc_mint 229 | { 230 | (raydium_library::amm::utils::SwapDirection::Coin2PC, true) 231 | } else { 232 | (raydium_library::amm::utils::SwapDirection::PC2Coin, false) 233 | }; 234 | 235 | let amount_specified_is_input = swap_input.mode.amount_specified_is_input(); 236 | let (other_amount, other_amount_threshold) = raydium_library::amm::swap_with_slippage( 237 | amm_pool_pc_vault_amount, 238 | amm_pool_coin_vault_amount, 239 | amm.fees.swap_fee_numerator, 240 | amm.fees.swap_fee_denominator, 241 | direction, 242 | swap_input.amount, 243 | amount_specified_is_input, 244 | swap_input.slippage_bps as u64, 245 | )?; 246 | log::debug!( 247 | "raw quote: {}. raw other_amount_threshold: {}", 248 | other_amount, 249 | other_amount_threshold 250 | ); 251 | 252 | Ok(RaydiumAmmQuote { 253 | market: pool_id, 254 | input_mint: swap_input.input_token_mint, 255 | output_mint: swap_input.output_token_mint, 256 | amount: swap_input.amount, 257 | other_amount, 258 | other_amount_threshold, 259 | amount_specified_is_input, 260 | input_mint_decimals: if coin_to_pc { 261 | amm.coin_decimals 262 | } else { 263 | amm.pc_decimals 264 | } as u8, 265 | output_mint_decimals: if coin_to_pc { 266 | amm.pc_decimals 267 | } else { 268 | amm.coin_decimals 269 | } as u8, 270 | amm_keys, 271 | market_keys, 272 | }) 273 | } 274 | 275 | pub async fn swap_instructions( 276 | &self, 277 | input_pubkey: Pubkey, 278 | output: RaydiumAmmQuote, 279 | overrides: Option<&SwapConfigOverrides>, 280 | ) -> anyhow::Result> { 281 | let builder = self.make_swap(input_pubkey, output, overrides).await?; 282 | builder.build_instructions() 283 | } 284 | 285 | pub async fn swap_transaction( 286 | &self, 287 | input_pubkey: Pubkey, 288 | output: RaydiumAmmQuote, 289 | overrides: Option<&SwapConfigOverrides>, 290 | ) -> anyhow::Result { 291 | let builder = self.make_swap(input_pubkey, output, overrides).await?; 292 | builder.build_transaction(Some(&input_pubkey), None) 293 | } 294 | 295 | pub fn update_config(&mut self, config: &SwapConfig) { 296 | self.config = *config; 297 | } 298 | 299 | async fn make_swap( 300 | &self, 301 | input_pubkey: Pubkey, 302 | output: RaydiumAmmQuote, 303 | overrides: Option<&SwapConfigOverrides>, 304 | ) -> anyhow::Result { 305 | let priority_fee = overrides 306 | .and_then(|o| o.priority_fee) 307 | .or(self.config.priority_fee); 308 | let cu_limits = overrides 309 | .and_then(|o| o.cu_limits) 310 | .or(self.config.cu_limits); 311 | let wrap_and_unwrap_sol = overrides 312 | .and_then(|o| o.wrap_and_unwrap_sol) 313 | .or(self.config.wrap_and_unwrap_sol) 314 | .unwrap_or(true); 315 | 316 | let mut builder = SwapInstructionsBuilder::default(); 317 | let _associated_accounts = builder.handle_token_wrapping_and_accounts_creation( 318 | input_pubkey, 319 | wrap_and_unwrap_sol, 320 | if output.amount_specified_is_input { 321 | output.amount 322 | } else { 323 | output.other_amount 324 | }, 325 | output.input_mint, 326 | output.output_mint, 327 | spl_token::ID, 328 | spl_token::ID, 329 | None, 330 | )?; 331 | let instruction = swap_instruction( 332 | &RAYDIUM_LIQUIDITY_POOL_V4_PROGRAM_ID, 333 | &output.amm_keys, 334 | &output.market_keys, 335 | &input_pubkey, 336 | &spl_associated_token_account::get_associated_token_address( 337 | &input_pubkey, 338 | &output.input_mint, 339 | ), 340 | &spl_associated_token_account::get_associated_token_address( 341 | &input_pubkey, 342 | &output.output_mint, 343 | ), 344 | output.amount, 345 | output.other_amount_threshold, 346 | output.amount_specified_is_input, 347 | )?; 348 | builder.swap_instruction = Some(instruction); 349 | 350 | let compute_units = builder 351 | .handle_compute_units_params(cu_limits, &self.client, input_pubkey) 352 | .await?; 353 | builder.handle_priority_fee_params(priority_fee, compute_units, input_pubkey)?; 354 | 355 | Ok(builder) 356 | } 357 | } 358 | 359 | #[derive(Debug)] 360 | pub struct RaydiumAmmQuote { 361 | /// The address of the amm pool 362 | pub market: Pubkey, 363 | /// The input mint 364 | pub input_mint: Pubkey, 365 | /// The output mint, 366 | pub output_mint: Pubkey, 367 | /// The amount specified 368 | pub amount: u64, 369 | /// The other amount 370 | pub other_amount: u64, 371 | /// The other amount with slippage 372 | pub other_amount_threshold: u64, 373 | /// Whether the amount specified is in terms of the input token 374 | pub amount_specified_is_input: bool, 375 | /// The input mint decimals 376 | pub input_mint_decimals: u8, 377 | /// The output mint decimals 378 | pub output_mint_decimals: u8, 379 | /// Amm keys 380 | pub amm_keys: AmmKeys, 381 | /// Market keys 382 | pub market_keys: MarketKeys, 383 | } 384 | 385 | #[derive(Debug, Clone, Copy)] 386 | pub struct MarketKeys { 387 | pub event_queue: Pubkey, 388 | pub bids: Pubkey, 389 | pub asks: Pubkey, 390 | pub coin_vault: Pubkey, 391 | pub pc_vault: Pubkey, 392 | pub vault_signer_key: Pubkey, 393 | } 394 | 395 | #[allow(clippy::too_many_arguments)] 396 | fn swap_instruction( 397 | amm_program: &Pubkey, 398 | amm_keys: &AmmKeys, 399 | market_keys: &MarketKeys, 400 | user_owner: &Pubkey, 401 | user_source: &Pubkey, 402 | user_destination: &Pubkey, 403 | amount_specified: u64, 404 | other_amount_threshold: u64, 405 | swap_base_in: bool, 406 | ) -> anyhow::Result { 407 | let swap_instruction = if swap_base_in { 408 | raydium_amm::instruction::swap_base_in( 409 | amm_program, 410 | &amm_keys.amm_pool, 411 | &amm_keys.amm_authority, 412 | &amm_keys.amm_open_order, 413 | &amm_keys.amm_coin_vault, 414 | &amm_keys.amm_pc_vault, 415 | &amm_keys.market_program, 416 | &amm_keys.market, 417 | &market_keys.bids, 418 | &market_keys.asks, 419 | &market_keys.event_queue, 420 | &market_keys.coin_vault, 421 | &market_keys.pc_vault, 422 | &market_keys.vault_signer_key, 423 | user_source, 424 | user_destination, 425 | user_owner, 426 | amount_specified, 427 | other_amount_threshold, 428 | )? 429 | } else { 430 | raydium_amm::instruction::swap_base_out( 431 | amm_program, 432 | &amm_keys.amm_pool, 433 | &amm_keys.amm_authority, 434 | &amm_keys.amm_open_order, 435 | &amm_keys.amm_coin_vault, 436 | &amm_keys.amm_pc_vault, 437 | &amm_keys.market_program, 438 | &amm_keys.market, 439 | &market_keys.bids, 440 | &market_keys.asks, 441 | &market_keys.event_queue, 442 | &market_keys.coin_vault, 443 | &market_keys.pc_vault, 444 | &market_keys.vault_signer_key, 445 | user_source, 446 | user_destination, 447 | user_owner, 448 | other_amount_threshold, 449 | amount_specified, 450 | )? 451 | }; 452 | 453 | Ok(swap_instruction) 454 | } 455 | 456 | impl From<&raydium_library::amm::MarketPubkeys> for MarketKeys { 457 | fn from(keys: &raydium_library::amm::MarketPubkeys) -> Self { 458 | MarketKeys { 459 | event_queue: *keys.event_q, 460 | bids: *keys.bids, 461 | asks: *keys.asks, 462 | coin_vault: *keys.coin_vault, 463 | pc_vault: *keys.pc_vault, 464 | vault_signer_key: *keys.vault_signer_key, 465 | } 466 | } 467 | } 468 | impl From<&crate::api_v3::response::pools::standard::MarketKeys> for MarketKeys { 469 | fn from(keys: &crate::api_v3::response::pools::standard::MarketKeys) -> Self { 470 | MarketKeys { 471 | event_queue: keys.market_event_queue, 472 | bids: keys.market_bids, 473 | asks: keys.market_asks, 474 | coin_vault: keys.market_base_vault, 475 | pc_vault: keys.market_quote_vault, 476 | vault_signer_key: keys.market_authority, 477 | } 478 | } 479 | } 480 | impl TryFrom<&crate::api_v3::response::ApiV3StandardPoolKeys> for MarketKeys { 481 | type Error = anyhow::Error; 482 | 483 | fn try_from( 484 | keys: &crate::api_v3::response::ApiV3StandardPoolKeys, 485 | ) -> Result { 486 | let keys = keys 487 | .keys 488 | .market 489 | .as_ref() 490 | .context("market keys should be present for amm")?; 491 | Ok(MarketKeys::from(keys)) 492 | } 493 | } 494 | 495 | impl TryFrom<&crate::api_v3::response::ApiV3StandardPoolKeys> for AmmKeys { 496 | type Error = anyhow::Error; 497 | 498 | fn try_from( 499 | keys: &crate::api_v3::response::ApiV3StandardPoolKeys, 500 | ) -> Result { 501 | let market_keys = keys 502 | .keys 503 | .market 504 | .as_ref() 505 | .context("market keys should be present for amm")?; 506 | Ok(AmmKeys { 507 | amm_pool: keys.id, 508 | amm_coin_mint: keys.mint_a.address, 509 | amm_pc_mint: keys.mint_b.address, 510 | amm_authority: keys.keys.authority, 511 | amm_target: keys 512 | .keys 513 | .target_orders 514 | .context("target orders should be present for amm")?, 515 | amm_coin_vault: keys.vault.a, 516 | amm_pc_vault: keys.vault.b, 517 | amm_lp_mint: keys.keys.mint_lp.address, 518 | amm_open_order: keys 519 | .keys 520 | .open_orders 521 | .context("open orders should be present for amm")?, 522 | market_program: market_keys.market_program_id, 523 | market: market_keys.market_id, 524 | nonce: 0, // random 525 | }) 526 | } 527 | } 528 | --------------------------------------------------------------------------------