├── Cargo.toml ├── README.md └── src ├── arbitrageur.rs ├── constants.rs ├── formula ├── base.rs ├── clmm │ ├── concentrated_liquidity.rs │ ├── constant.rs │ ├── full_math.rs │ ├── mod.rs │ ├── orca_swap_state.rs │ ├── orca_tick_math.rs │ ├── raydium_sqrt_price_math.rs │ ├── raydium_swap_state.rs │ ├── raydium_tick_array.rs │ ├── raydium_tick_math.rs │ ├── test │ │ ├── complex_swap_test.rs │ │ ├── liquidity_test_fixture.rs │ │ ├── mod.rs │ │ ├── orca_swap_test.rs │ │ ├── raydium_swap_test.rs │ │ └── swap_test_fixture.rs │ └── u256_math.rs ├── constant_product.rs ├── dlmm │ ├── bin.rs │ ├── bin_array_bitmap_extension.rs │ ├── constant.rs │ ├── mod.rs │ ├── safe_math.rs │ ├── test │ │ ├── meteora_test.rs │ │ └── mod.rs │ ├── u128x128_math.rs │ ├── u64x64_math.rs │ └── utils_math.rs ├── meteora_dlmm.rs ├── mod.rs ├── openbook │ ├── math.rs │ ├── mod.rs │ └── openbook_processor.rs ├── orca_clmm.rs ├── raydium_clmm.rs └── raydium_openbook.rs ├── main.rs ├── observer.rs ├── path.rs ├── probe.rs ├── pubkey ├── meteora.json ├── orca.json └── raydium.json ├── struct ├── account.rs ├── market.rs ├── mod.rs ├── pools │ ├── lifinity.rs │ ├── meteora.rs │ ├── mod.rs │ ├── orca.rs │ └── raydium.rs ├── resolver.rs └── token.rs ├── temp.rs └── utils.rs /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sol-arb-bot" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | reqwest = { version = "0.12.7", features = ["json"] } 8 | tokio = { version = "1.40.0", features = ["full"] } 9 | serde = { version = "1.0.210", features = ["derive"] } 10 | serde_json = "1.0.128" 11 | num-bigfloat = "1.7.1" 12 | num-bigint = "0.4.6" 13 | num_enum = "0.7.3" 14 | solana-sdk = "2.0.10" 15 | solana-client = "2.0.10" 16 | bincode = "1.3.3" 17 | base64 = "0.22.1" 18 | arrayref = "0.3.9" 19 | time = "0.3.36" 20 | dyn-clone = "1.0.17" 21 | bytemuck = "1.18.0" 22 | num-traits = "0.2.19" 23 | uint = { version = "0.10.0", default-features = false } 24 | rand = "0.8.5" 25 | num-integer = "0.1.45" 26 | ruint = "1.3.0" 27 | anyhow = "1.0.71" 28 | serum_dex = { version = "0.5.10", git = "https://github.com/raydium-io/openbook-dex", features=["no-entrypoint", "program"] } 29 | log = "0.4.22" 30 | 31 | [patch.crates-io] 32 | curve25519-dalek = { git = 'https://github.com/ivs/curve25519-dalek', branch='rustls-dep-hell' } 33 | aes-gcm-siv = { git = 'https://github.com/ivs/AEADs', branch='rustls-dep-hell' } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana DEX Arbitrage Bot 2 | 3 | 4 | ## Objective 5 | * Compare price difference between source pool and destination pool. 6 | * Send transaction and execute swap if exchange profit found. 7 | 8 | This project is still working on. 9 | 10 | ## How does it work? 11 | 1. Get all available pool accounts from json files that have various market accounts (e.g. Orca, Raydium, Meteora and else) 12 | 2. Fetch all pool account data and put them in a vector. (Run only first time) 13 | 3. Fetch all related accounts data from vector declared above and put them in another vector. (Run at intervals) 14 | 4. Resolve all available path refer to pool accounts. 15 | 5. Run arbitrage once all related accounts data fetched. 16 | 17 | 18 | ## Notice 19 | * Some of dexes have different swap formula, swap result may not be accurate. 20 | * Even if some of dexes have same swap formula, they may have different setting for each pools and different logics, so swap result may not be same. -------------------------------------------------------------------------------- /src/arbitrageur.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, Mutex}; 3 | use solana_sdk::pubkey::Pubkey; 4 | use crate::r#struct::account::{DeserializedAccount, DeserializedPoolAccount}; 5 | 6 | pub struct Arbitrageur { 7 | shared_account_bin: Arc>>, 8 | path_list: Arc>>> 9 | } 10 | 11 | impl Arbitrageur { 12 | pub fn new( 13 | shared_account_bin: Arc>>, 14 | path_list: Arc>>> 15 | ) -> Arbitrageur { 16 | Arbitrageur { 17 | shared_account_bin, 18 | path_list 19 | } 20 | } 21 | 22 | pub fn arbitrage_single( 23 | &self, 24 | target_mint: Pubkey, 25 | init_amount: u64 26 | ) { 27 | if let Some(path_list) = self.path_list.lock().unwrap().get(&target_mint) { 28 | path_list.iter().for_each(|pool| { 29 | let accounts = self.shared_account_bin.lock().unwrap().iter().filter(|account| { 30 | account.get_market() == pool.market 31 | }).map(|account| { 32 | account.clone() 33 | }).collect::>(); 34 | 35 | pool.operation.swap(&accounts); 36 | }) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | pub static RPC_URL: &str = "https://go.getblock.io/bd8eab2bbe6e448b84ca2ae3b282b819"; 2 | pub static MAX_DEPTH: usize = 4; 3 | 4 | pub static TOKEN_PROGRAM_PUBKEY: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; 5 | pub static TOKEN_ACCOUNT_DATA_LEN: usize = 165; 6 | 7 | // Raydium 8 | pub const RAYDIUM_CLMM_PROGRAM_PUBKEY: &str = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"; 9 | pub const RAYDIUM_OPEN_BOOK_PROGRAM_PUBKEY: &str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"; 10 | pub const RAYDIUM_CLMM_DATA_LEN: usize = 1544; 11 | pub const RAYDIUM_OPEN_BOOK_DATA_LEN: usize = 752; 12 | pub const RAYDIUM_CLMM_AMM_CONFIG: usize = 117; 13 | pub const RAYDIUM_CLMM_OBSERVATION_KEY: usize = 4483; 14 | pub const RAYDIUM_CLMM_TICK_ARRAY_STATE: usize = 10240; 15 | pub const RAYDIUM_CLMM_TICK_ARRAY_BITMAP_EXTENSION: usize = 1832; 16 | 17 | 18 | 19 | pub const ORCA_CLMM_TICK_ARRAY: usize = 9988; 20 | pub const ORCA_CLMM_WHIRLPOOL_CONFIG: usize = 108; 21 | 22 | 23 | pub const METEORA_DLMM_PROGRAM_PUBKEY: &str = "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo"; -------------------------------------------------------------------------------- /src/formula/base.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Default, Eq, PartialEq, Debug)] 2 | pub enum Formula { 3 | #[default] 4 | UnknownFormula, 5 | ConstantProduct, 6 | ConcentratedLiquidity, 7 | DynamicLiquidity, 8 | OpenBook 9 | } 10 | 11 | pub trait SwapSimulator { 12 | } -------------------------------------------------------------------------------- /src/formula/clmm/constant.rs: -------------------------------------------------------------------------------- 1 | use crate::formula::clmm::u256_math::U128; 2 | 3 | pub const MIN_TICK: i32 = -443636; 4 | pub const MAX_TICK: i32 = -MIN_TICK; 5 | pub const TICK_ARRAY_SIZE: i32 = 60; 6 | pub const TICK_ARRAY_BITMAP_SIZE: i32 = 512; 7 | pub const NUM_64: U128 = U128([64, 0]); 8 | pub const REWARD_NUM: usize = 3; 9 | 10 | pub const BIT_PRECISION: u32 = 16; 11 | pub const FEE_RATE_DENOMINATOR_VALUE: u32 = 1_000_000; 12 | pub const TICK_ARRAY_SIZE_USIZE: usize = 60; 13 | pub const MIN_SQRT_PRICE_X64: u128 = 4295048016; 14 | pub const MAX_SQRT_PRICE_X64: u128 = 79226673521066979257578248091; 15 | pub const ORCA_MAX_SQRT_PRICE_X64: u128 = 79226673515401279992447579055; 16 | 17 | pub const POOL_SEED: &str = "pool"; 18 | pub const POOL_VAULT_SEED: &str = "pool_vault"; 19 | pub const POOL_REWARD_VAULT_SEED: &str = "pool_reward_vault"; 20 | pub const POOL_TICK_ARRAY_BITMAP_SEED: &str = "pool_tick_array_bitmap_extension"; 21 | pub const TICK_ARRAY_SEED: &str = "tick_array"; -------------------------------------------------------------------------------- /src/formula/clmm/full_math.rs: -------------------------------------------------------------------------------- 1 | //! A custom implementation of https://github.com/sdroege/rust-muldiv to support phantom overflow resistant 2 | //! multiply-divide operations. This library uses U128 in place of u128 for u64 operations, 3 | //! and supports U128 operations. 4 | //! 5 | 6 | use crate::formula::clmm::u256_math::{U128, U256, U512}; 7 | 8 | /// Trait for calculating `val * num / denom` with different rounding modes and overflow 9 | /// protection. 10 | /// 11 | /// Implementations of this trait have to ensure that even if the result of the multiplication does 12 | /// not fit into the type, as long as it would fit after the division the correct result has to be 13 | /// returned instead of `None`. `None` only should be returned if the overall result does not fit 14 | /// into the type. 15 | /// 16 | /// This specifically means that e.g. the `u64` implementation must, depending on the arguments, be 17 | /// able to do 128 bit integer multiplication. 18 | pub trait MulDiv { 19 | /// Output type for the methods of this trait. 20 | type Output; 21 | 22 | /// Calculates `floor(val * num / denom)`, i.e. the largest integer less than or equal to the 23 | /// result of the division. 24 | /// 25 | /// ## Example 26 | /// 27 | /// ```rust 28 | /// use libraries::full_math::MulDiv; 29 | /// 30 | /// # fn main() { 31 | /// let x = 3i8.mul_div_floor(4, 2); 32 | /// assert_eq!(x, Some(6)); 33 | /// 34 | /// let x = 5i8.mul_div_floor(2, 3); 35 | /// assert_eq!(x, Some(3)); 36 | /// 37 | /// let x = (-5i8).mul_div_floor(2, 3); 38 | /// assert_eq!(x, Some(-4)); 39 | /// 40 | /// let x = 3i8.mul_div_floor(3, 2); 41 | /// assert_eq!(x, Some(4)); 42 | /// 43 | /// let x = (-3i8).mul_div_floor(3, 2); 44 | /// assert_eq!(x, Some(-5)); 45 | /// 46 | /// let x = 127i8.mul_div_floor(4, 3); 47 | /// assert_eq!(x, None); 48 | /// # } 49 | /// ``` 50 | fn mul_div_floor(self, num: RHS, denom: RHS) -> Option; 51 | 52 | /// Calculates `ceil(val * num / denom)`, i.e. the the smallest integer greater than or equal to 53 | /// the result of the division. 54 | /// 55 | /// ## Example 56 | /// 57 | /// ```rust 58 | /// use libraries::full_math::MulDiv; 59 | /// 60 | /// # fn main() { 61 | /// let x = 3i8.mul_div_ceil(4, 2); 62 | /// assert_eq!(x, Some(6)); 63 | /// 64 | /// let x = 5i8.mul_div_ceil(2, 3); 65 | /// assert_eq!(x, Some(4)); 66 | /// 67 | /// let x = (-5i8).mul_div_ceil(2, 3); 68 | /// assert_eq!(x, Some(-3)); 69 | /// 70 | /// let x = 3i8.mul_div_ceil(3, 2); 71 | /// assert_eq!(x, Some(5)); 72 | /// 73 | /// let x = (-3i8).mul_div_ceil(3, 2); 74 | /// assert_eq!(x, Some(-4)); 75 | /// 76 | /// let x = (127i8).mul_div_ceil(4, 3); 77 | /// assert_eq!(x, None); 78 | /// # } 79 | /// ``` 80 | fn mul_div_ceil(self, num: RHS, denom: RHS) -> Option; 81 | 82 | /// Return u64 not out of bounds 83 | fn to_underflow_u64(self) -> u64; 84 | } 85 | 86 | pub trait Upcast256 { 87 | fn as_u256(self) -> U256; 88 | } 89 | impl Upcast256 for U128 { 90 | fn as_u256(self) -> U256 { 91 | U256([self.0[0], self.0[1], 0, 0]) 92 | } 93 | } 94 | 95 | pub trait Downcast256 { 96 | /// Unsafe cast to U128 97 | /// Bits beyond the 128th position are lost 98 | fn as_u128(self) -> U128; 99 | } 100 | impl Downcast256 for U256 { 101 | fn as_u128(self) -> U128 { 102 | U128([self.0[0], self.0[1]]) 103 | } 104 | } 105 | 106 | pub trait Upcast512 { 107 | fn as_u512(self) -> U512; 108 | } 109 | impl Upcast512 for U256 { 110 | fn as_u512(self) -> U512 { 111 | U512([self.0[0], self.0[1], self.0[2], self.0[3], 0, 0, 0, 0]) 112 | } 113 | } 114 | 115 | pub trait Downcast512 { 116 | /// Unsafe cast to U256 117 | /// Bits beyond the 256th position are lost 118 | fn as_u256(self) -> U256; 119 | } 120 | impl Downcast512 for U512 { 121 | fn as_u256(self) -> U256 { 122 | U256([self.0[0], self.0[1], self.0[2], self.0[3]]) 123 | } 124 | } 125 | 126 | impl MulDiv for u64 { 127 | type Output = u64; 128 | 129 | fn mul_div_floor(self, num: Self, denom: Self) -> Option { 130 | assert_ne!(denom, 0); 131 | let r = (U128::from(self) * U128::from(num)) / U128::from(denom); 132 | if r > U128::from(u64::MAX) { 133 | None 134 | } else { 135 | Some(r.as_u64()) 136 | } 137 | } 138 | 139 | fn mul_div_ceil(self, num: Self, denom: Self) -> Option { 140 | assert_ne!(denom, 0); 141 | let r = (U128::from(self) * U128::from(num) + U128::from(denom - 1)) / U128::from(denom); 142 | if r > U128::from(u64::MAX) { 143 | None 144 | } else { 145 | Some(r.as_u64()) 146 | } 147 | } 148 | 149 | fn to_underflow_u64(self) -> u64 { 150 | self 151 | } 152 | } 153 | 154 | impl MulDiv for U128 { 155 | type Output = U128; 156 | 157 | fn mul_div_floor(self, num: Self, denom: Self) -> Option { 158 | assert_ne!(denom, U128::default()); 159 | let r = ((self.as_u256()) * (num.as_u256())) / (denom.as_u256()); 160 | if r > U128::MAX.as_u256() { 161 | None 162 | } else { 163 | Some(r.as_u128()) 164 | } 165 | } 166 | 167 | fn mul_div_ceil(self, num: Self, denom: Self) -> Option { 168 | assert_ne!(denom, U128::default()); 169 | let r = (self.as_u256() * num.as_u256() + (denom - 1).as_u256()) / denom.as_u256(); 170 | if r > U128::MAX.as_u256() { 171 | None 172 | } else { 173 | Some(r.as_u128()) 174 | } 175 | } 176 | 177 | fn to_underflow_u64(self) -> u64 { 178 | if self < U128::from(u64::MAX) { 179 | self.as_u64() 180 | } else { 181 | 0 182 | } 183 | } 184 | } 185 | 186 | impl MulDiv for U256 { 187 | type Output = U256; 188 | 189 | fn mul_div_floor(self, num: Self, denom: Self) -> Option { 190 | assert_ne!(denom, U256::default()); 191 | let r = (self.as_u512() * num.as_u512()) / denom.as_u512(); 192 | if r > U256::MAX.as_u512() { 193 | None 194 | } else { 195 | Some(r.as_u256()) 196 | } 197 | } 198 | 199 | fn mul_div_ceil(self, num: Self, denom: Self) -> Option { 200 | assert_ne!(denom, U256::default()); 201 | let r = (self.as_u512() * num.as_u512() + (denom - 1).as_u512()) / denom.as_u512(); 202 | if r > U256::MAX.as_u512() { 203 | None 204 | } else { 205 | Some(r.as_u256()) 206 | } 207 | } 208 | 209 | fn to_underflow_u64(self) -> u64 { 210 | if self < U256::from(u64::MAX) { 211 | self.as_u64() 212 | } else { 213 | 0 214 | } 215 | } 216 | } 217 | 218 | // #[cfg(test)] 219 | // mod muldiv_u64_tests { 220 | // use super::*; 221 | // 222 | // use quickcheck::{quickcheck, Arbitrary, Gen}; 223 | // 224 | // #[derive(Debug, Clone, Copy, PartialEq, Eq)] 225 | // struct NonZero(u64); 226 | // 227 | // impl Arbitrary for NonZero { 228 | // fn arbitrary(g: &mut G) -> Self { 229 | // loop { 230 | // let v = u64::arbitrary(g); 231 | // if v != 0 { 232 | // return NonZero(v); 233 | // } 234 | // } 235 | // } 236 | // } 237 | // 238 | // quickcheck! { 239 | // fn scale_floor(val: u64, num: u64, den: NonZero) -> bool { 240 | // let res = val.mul_div_floor(num, den.0); 241 | // 242 | // let expected = (U128::from(val) * U128::from(num)) / U128::from(den.0); 243 | // 244 | // if expected > U128::from(u64::MAX) { 245 | // res.is_none() 246 | // } else { 247 | // res == Some(expected.as_u64()) 248 | // } 249 | // } 250 | // } 251 | // 252 | // quickcheck! { 253 | // fn scale_ceil(val: u64, num: u64, den: NonZero) -> bool { 254 | // let res = val.mul_div_ceil(num, den.0); 255 | // 256 | // let mut expected = (U128::from(val) * U128::from(num)) / U128::from(den.0); 257 | // let expected_rem = (U128::from(val) * U128::from(num)) % U128::from(den.0); 258 | // 259 | // if expected_rem != U128::default() { 260 | // expected += U128::from(1) 261 | // } 262 | // 263 | // if expected > U128::from(u64::MAX) { 264 | // res.is_none() 265 | // } else { 266 | // res == Some(expected.as_u64()) 267 | // } 268 | // } 269 | // } 270 | // } 271 | // 272 | // #[cfg(test)] 273 | // mod muldiv_u128_tests { 274 | // use super::*; 275 | // 276 | // use quickcheck::{quickcheck, Arbitrary, Gen}; 277 | // 278 | // #[derive(Debug, Clone, Copy, PartialEq, Eq)] 279 | // struct NonZero(U128); 280 | // 281 | // impl Arbitrary for NonZero { 282 | // fn arbitrary(g: &mut G) -> Self { 283 | // loop { 284 | // let v = U128::from(u128::arbitrary(g)); 285 | // if v != U128::default() { 286 | // return NonZero(v); 287 | // } 288 | // } 289 | // } 290 | // } 291 | // 292 | // impl Arbitrary for U128 { 293 | // fn arbitrary(g: &mut G) -> Self { 294 | // loop { 295 | // let v = U128::from(u128::arbitrary(g)); 296 | // if v != U128::default() { 297 | // return v; 298 | // } 299 | // } 300 | // } 301 | // } 302 | // 303 | // quickcheck! { 304 | // fn scale_floor(val: U128, num: U128, den: NonZero) -> bool { 305 | // let res = val.mul_div_floor(num, den.0); 306 | // 307 | // let expected = ((val.as_u256()) * (num.as_u256())) / (den.0.as_u256()); 308 | // 309 | // if expected > U128::MAX.as_u256() { 310 | // res.is_none() 311 | // } else { 312 | // res == Some(expected.as_u128()) 313 | // } 314 | // } 315 | // } 316 | // 317 | // quickcheck! { 318 | // fn scale_ceil(val: U128, num: U128, den: NonZero) -> bool { 319 | // let res = val.mul_div_ceil(num, den.0); 320 | // 321 | // let mut expected = ((val.as_u256()) * (num.as_u256())) / (den.0.as_u256()); 322 | // let expected_rem = ((val.as_u256()) * (num.as_u256())) % (den.0.as_u256()); 323 | // 324 | // if expected_rem != U256::default() { 325 | // expected += U256::from(1) 326 | // } 327 | // 328 | // if expected > U128::MAX.as_u256() { 329 | // res.is_none() 330 | // } else { 331 | // res == Some(expected.as_u128()) 332 | // } 333 | // } 334 | // } 335 | // } 336 | -------------------------------------------------------------------------------- /src/formula/clmm/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod raydium_sqrt_price_math; 2 | pub mod raydium_tick_array; 3 | pub mod constant; 4 | pub mod raydium_swap_state; 5 | pub mod concentrated_liquidity; 6 | pub mod orca_swap_state; 7 | pub mod test; 8 | pub mod u256_math; 9 | pub mod raydium_tick_math; 10 | pub mod orca_tick_math; 11 | pub mod full_math; 12 | -------------------------------------------------------------------------------- /src/formula/clmm/raydium_sqrt_price_math.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Add; 2 | use std::str::FromStr; 3 | 4 | use num_bigfloat::BigFloat; 5 | use num_bigint::BigInt; 6 | 7 | // 2^64 = 18446744073709551616 8 | pub const Q64: u128 = 18_446_744_073_709_551_616u128; 9 | 10 | pub fn tick_to_sqrt_price_x64(tick: &i32) -> Option { 11 | BigFloat::from(1.0001f64).pow(&BigFloat::from(*tick).div(&BigFloat::from(2))).mul(&BigFloat::from(Q64)).to_u128() 12 | } 13 | 14 | pub fn sqrt_price_x64_to_tick(sqrt_price_x64: &u128) -> Option { 15 | let tick = BigFloat::from(*sqrt_price_x64).div(&BigFloat::from(Q64)).log(&BigFloat::from(1.0001f64)).mul(&BigFloat::from(2u8)).floor().to_i64(); 16 | match i32::try_from(tick?) { 17 | Ok(tick) => { Some(tick) } 18 | Err(_) => { None } 19 | } 20 | } 21 | 22 | pub fn price_to_sqrt_price_x64(price: &f64, decimal_diff: &i8) -> u128 { 23 | let decimals = BigFloat::from(10u32).pow(&BigFloat::from(*decimal_diff)); 24 | BigFloat::from(*price).div(&decimals).sqrt().mul(&BigFloat::from(Q64)).floor().to_u128().unwrap() 25 | } 26 | 27 | pub fn sqrt_price_x64_to_price(sqrt_price_x64: &u128, decimal_diff: &i8) -> f64 { 28 | let decimals = BigFloat::from(10u32).pow(&BigFloat::from(*decimal_diff)); 29 | BigFloat::from(*sqrt_price_x64).div(&BigFloat::from(Q64)).pow(&BigFloat::from(2u8)).mul(&decimals).to_f64() 30 | } 31 | 32 | pub fn get_next_sqrt_price_from_input( 33 | sqrt_price_x64: u128, 34 | liquidity: u128, 35 | amount_in: u64, 36 | zero_for_one: bool, 37 | ) -> u128 { 38 | if zero_for_one { 39 | get_next_sqrt_price_from_amount_0_rounding_up(sqrt_price_x64, liquidity, amount_in, true) 40 | } 41 | else { 42 | get_next_sqrt_price_from_amount_1_rounding_down(sqrt_price_x64, liquidity, amount_in, true) 43 | } 44 | } 45 | 46 | pub fn get_next_sqrt_price_from_output( 47 | sqrt_price_x64: u128, 48 | liquidity: u128, 49 | amount_out: u64, 50 | zero_for_one: bool, 51 | ) -> u128 { 52 | if zero_for_one { 53 | get_next_sqrt_price_from_amount_1_rounding_down(sqrt_price_x64, liquidity, amount_out, false) 54 | } 55 | else { 56 | get_next_sqrt_price_from_amount_0_rounding_up(sqrt_price_x64, liquidity, amount_out, false) 57 | } 58 | } 59 | 60 | pub fn get_next_sqrt_price_from_amount_0_rounding_up( 61 | sqrt_price_x64: u128, 62 | liquidity: u128, 63 | amount: u64, 64 | add: bool, 65 | ) -> u128 { 66 | if amount == 0 { 67 | return sqrt_price_x64; 68 | } 69 | 70 | let liquidity_shifted = BigInt::from(liquidity) << 64i32; 71 | let numerator = BigFloat::from_str(liquidity_shifted.to_string().as_str()).unwrap(); 72 | let amount_bf = BigFloat::from(amount); 73 | let sqrt_price_x64_bf = BigFloat::from(sqrt_price_x64); 74 | 75 | if add { 76 | let product = amount_bf.mul(&sqrt_price_x64_bf); 77 | let denominator = numerator.add(product); 78 | if denominator >= numerator { 79 | return numerator.mul(&sqrt_price_x64_bf).div(&denominator).ceil().to_u128().unwrap() 80 | } 81 | 82 | let value = numerator.div(&sqrt_price_x64_bf).add(amount_bf); 83 | numerator.div(&value).add(BigFloat::from((numerator % value > BigFloat::default()) as u8)).to_u128().unwrap() 84 | } 85 | else { 86 | let product = amount_bf.mul(&sqrt_price_x64_bf); 87 | let denominator = numerator.sub(&product); 88 | numerator.mul(&sqrt_price_x64_bf).div(&denominator).ceil().to_u128().unwrap() 89 | } 90 | } 91 | 92 | pub fn get_next_sqrt_price_from_amount_1_rounding_down( 93 | sqrt_price_x64: u128, 94 | liquidity: u128, 95 | amount: u64, 96 | add: bool, 97 | ) -> u128 { 98 | let sqrt_price_x64_bf = BigFloat::from(sqrt_price_x64); 99 | let liquidity_bf = BigFloat::from(liquidity); 100 | 101 | if add { 102 | let amount_shifted = BigInt::from(amount) << 64i32; 103 | let quotient = BigFloat::from_str(amount_shifted.to_string().as_str()).unwrap().div(&liquidity_bf); 104 | sqrt_price_x64_bf.add("ient).to_u128().unwrap() 105 | } else { 106 | let amount_shifted = BigInt::from(amount) << 64i32; // U256::from(u128::from(amount) << fixed_point_64::RESOLUTION) 107 | let value = BigFloat::from_str(amount_shifted.to_string().as_str()).unwrap(); 108 | let quotient = value.div(&liquidity_bf).add(BigFloat::from((value % liquidity_bf > BigFloat::default()) as u8)).floor(); 109 | sqrt_price_x64_bf.sub("ient).to_u128().unwrap() 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod test { 115 | use crate::formula::clmm::constant::{MAX_SQRT_PRICE_X64, MIN_SQRT_PRICE_X64}; 116 | use crate::formula::clmm::raydium_sqrt_price_math::{price_to_sqrt_price_x64, sqrt_price_x64_to_price, sqrt_price_x64_to_tick, tick_to_sqrt_price_x64}; 117 | 118 | #[test] 119 | fn test_tick_to_sqrt_price_x64() { 120 | let tick = -18867i32; 121 | let sqrt_price_x64 = tick_to_sqrt_price_x64(&tick); 122 | println!("{}", sqrt_price_x64.unwrap()); 123 | } 124 | 125 | #[test] 126 | fn test_sqrt_price_x64_to_tick() { 127 | let sqrt_price_x64: Vec = vec![ 128 | 7182147241917313386, 129 | 7174399016327223095, 130 | 7174386368720733565, 131 | 7174388168782077692, 132 | 7174954712407921105, 133 | MAX_SQRT_PRICE_X64, 134 | MIN_SQRT_PRICE_X64 135 | ]; 136 | 137 | let ticks: Vec = vec![ 138 | -18867, 139 | -18889, 140 | -18889, 141 | -18889, 142 | -18887, 143 | 443636, 144 | -443636 145 | ]; 146 | 147 | sqrt_price_x64.iter().enumerate().for_each(|(index, p)| { 148 | let tick = sqrt_price_x64_to_tick(p); 149 | assert_eq!(tick.unwrap(), ticks[index]) 150 | }) 151 | } 152 | 153 | #[test] 154 | fn test_price_to_sqrt_price_x64() { 155 | let price = 151.37f64; 156 | let sqrt_price_x64 = price_to_sqrt_price_x64(&price, &-3i8); 157 | println!("{}", sqrt_price_x64) 158 | } 159 | 160 | #[test] 161 | fn test_sqrt_price_x64_to_price() { 162 | let sqrt_price_x64 = 7168359157675602364u128; 163 | let price = sqrt_price_x64_to_price(&sqrt_price_x64, &3i8); 164 | println!("{}", price) 165 | } 166 | } -------------------------------------------------------------------------------- /src/formula/clmm/raydium_swap_state.rs: -------------------------------------------------------------------------------- 1 | use std::mem::swap; 2 | use std::num::TryFromIntError; 3 | use num_bigfloat::BigFloat; 4 | use num_traits::ToPrimitive; 5 | use crate::formula::clmm::full_math::MulDiv; 6 | use crate::formula::clmm::raydium_sqrt_price_math::Q64; 7 | use crate::formula::clmm::raydium_tick_math::get_sqrt_price_at_tick; 8 | use crate::formula::clmm::u256_math::{U128, U256}; 9 | 10 | /* 11 | For Raydium Concentrated Liquidity pool 12 | */ 13 | 14 | 15 | #[derive(Debug)] 16 | pub struct SwapState { 17 | pub amount_specified_remaining: u64, 18 | pub amount_calculated: u64, 19 | pub sqrt_price_x64: u128, 20 | pub tick: i32, 21 | // pub fee_growth_global_x64: u128, 22 | pub fee_amount: u64, 23 | pub protocol_fee: u64, 24 | pub fund_fee: u64, 25 | pub liquidity: u128, 26 | } 27 | 28 | #[derive(Default)] 29 | pub struct StepComputations { 30 | pub sqrt_price_start_x64: u128, 31 | pub tick_next: i32, 32 | pub initialized: bool, 33 | pub sqrt_price_next_x64: u128, 34 | pub amount_in: u64, 35 | pub amount_out: u64, 36 | pub fee_amount: u64, 37 | } 38 | 39 | pub fn calculate_amount_in_range( 40 | sqrt_price_current_x64: u128, 41 | sqrt_price_target_x64: u128, 42 | liquidity: u128, 43 | zero_for_one: bool, 44 | is_base_input: bool, 45 | ) -> Result { 46 | if is_base_input { 47 | if zero_for_one { 48 | get_delta_amount_0_unsigned( 49 | sqrt_price_target_x64, 50 | sqrt_price_current_x64, 51 | liquidity, 52 | true 53 | ) 54 | } 55 | else { 56 | get_delta_amount_1_unsigned( 57 | sqrt_price_current_x64, 58 | sqrt_price_target_x64, 59 | liquidity, 60 | true 61 | ) 62 | } 63 | } 64 | else { 65 | if zero_for_one { 66 | get_delta_amount_1_unsigned( 67 | sqrt_price_target_x64, 68 | sqrt_price_current_x64, 69 | liquidity, 70 | false 71 | ) 72 | } 73 | else { 74 | get_delta_amount_0_unsigned( 75 | sqrt_price_current_x64, 76 | sqrt_price_target_x64, 77 | liquidity, 78 | false 79 | ) 80 | } 81 | } 82 | } 83 | 84 | pub fn get_liquidity_from_amounts( 85 | sqrt_ratio_x64: u128, 86 | mut sqrt_ratio_a_x64: u128, 87 | mut sqrt_ratio_b_x64: u128, 88 | amount_0: u64, 89 | amount_1: u64, 90 | ) -> u128 { 91 | // sqrt_ratio_a_x64 should hold the smaller value 92 | if sqrt_ratio_a_x64 > sqrt_ratio_b_x64 { 93 | std::mem::swap(&mut sqrt_ratio_a_x64, &mut sqrt_ratio_b_x64); 94 | }; 95 | 96 | if sqrt_ratio_x64 <= sqrt_ratio_a_x64 { 97 | // If P ≤ P_lower, only token_0 liquidity is active 98 | get_liquidity_from_amount_0(sqrt_ratio_a_x64, sqrt_ratio_b_x64, amount_0) 99 | } else if sqrt_ratio_x64 < sqrt_ratio_b_x64 { 100 | // If P_lower < P < P_upper, active liquidity is the minimum of the liquidity provided 101 | // by token_0 and token_1 102 | u128::min( 103 | get_liquidity_from_amount_0(sqrt_ratio_x64, sqrt_ratio_b_x64, amount_0), 104 | get_liquidity_from_amount_1(sqrt_ratio_a_x64, sqrt_ratio_x64, amount_1), 105 | ) 106 | } else { 107 | // If P ≥ P_upper, only token_1 liquidity is active 108 | get_liquidity_from_amount_1(sqrt_ratio_a_x64, sqrt_ratio_b_x64, amount_1) 109 | } 110 | } 111 | 112 | pub fn get_liquidity_from_amount_0( 113 | mut sqrt_ratio_a_x64: u128, 114 | mut sqrt_ratio_b_x64: u128, 115 | amount_0: u64, 116 | ) -> u128 { 117 | // sqrt_ratio_a_x64 should hold the smaller value 118 | if sqrt_ratio_a_x64 > sqrt_ratio_b_x64 { 119 | std::mem::swap(&mut sqrt_ratio_a_x64, &mut sqrt_ratio_b_x64); 120 | }; 121 | let intermediate = U128::from(sqrt_ratio_a_x64) 122 | .mul_div_floor( 123 | U128::from(sqrt_ratio_b_x64), 124 | U128::from(Q64), 125 | ) 126 | .unwrap(); 127 | 128 | U128::from(amount_0) 129 | .mul_div_floor( 130 | intermediate, 131 | U128::from(sqrt_ratio_b_x64 - sqrt_ratio_a_x64), 132 | ) 133 | .unwrap() 134 | .as_u128() 135 | } 136 | 137 | /// Computes the amount of liquidity received for a given amount of token_1 and price range 138 | /// Calculates ΔL = Δy / (√P_upper - √P_lower) 139 | pub fn get_liquidity_from_amount_1( 140 | mut sqrt_ratio_a_x64: u128, 141 | mut sqrt_ratio_b_x64: u128, 142 | amount_1: u64, 143 | ) -> u128 { 144 | // sqrt_ratio_a_x64 should hold the smaller value 145 | if sqrt_ratio_a_x64 > sqrt_ratio_b_x64 { 146 | swap(&mut sqrt_ratio_a_x64, &mut sqrt_ratio_b_x64); 147 | }; 148 | 149 | U128::from(amount_1) 150 | .mul_div_floor( 151 | U128::from(Q64), 152 | U128::from(sqrt_ratio_b_x64 - sqrt_ratio_a_x64), 153 | ) 154 | .unwrap() 155 | .as_u128() 156 | } 157 | 158 | pub fn get_delta_amount_0_unsigned( 159 | mut sqrt_ratio_a_x64: u128, 160 | mut sqrt_ratio_b_x64: u128, 161 | liquidity: u128, 162 | round_up: bool, 163 | ) -> Result { 164 | if sqrt_ratio_a_x64 > sqrt_ratio_b_x64 { 165 | swap(&mut sqrt_ratio_a_x64, &mut sqrt_ratio_b_x64) 166 | } 167 | 168 | let q64 = BigFloat::from(Q64); 169 | let num1 = BigFloat::from(liquidity).mul(&q64); 170 | let num2 = BigFloat::from(sqrt_ratio_b_x64 - sqrt_ratio_a_x64); 171 | 172 | if round_up { 173 | let res = u64::try_from( 174 | num1.mul(&num2).div(&BigFloat::from(sqrt_ratio_b_x64)).ceil().div(&BigFloat::from(sqrt_ratio_a_x64)).ceil().to_u128().unwrap() 175 | ); 176 | res 177 | } 178 | else { 179 | let res = u64::try_from( 180 | num1.mul(&num2).div(&BigFloat::from(sqrt_ratio_b_x64)).floor().div(&BigFloat::from(sqrt_ratio_a_x64)).to_u128().unwrap() 181 | ); 182 | res 183 | } 184 | } 185 | 186 | pub fn get_delta_amount_1_unsigned( 187 | mut sqrt_ratio_a_x64: u128, 188 | mut sqrt_ratio_b_x64: u128, 189 | liquidity: u128, 190 | round_up: bool, 191 | ) -> Result { 192 | if sqrt_ratio_a_x64 > sqrt_ratio_b_x64 { 193 | swap(&mut sqrt_ratio_a_x64, &mut sqrt_ratio_b_x64) 194 | } 195 | 196 | let q64 = BigFloat::from(Q64); 197 | if round_up { 198 | u64::try_from( 199 | BigFloat::from(liquidity).mul(&BigFloat::from(sqrt_ratio_b_x64).sub(&BigFloat::from(sqrt_ratio_a_x64))) 200 | .div(&q64) 201 | .ceil() 202 | .to_u128().unwrap() 203 | ) 204 | } 205 | else { 206 | u64::try_from( 207 | BigFloat::from(liquidity).mul(&BigFloat::from(sqrt_ratio_b_x64).sub(&BigFloat::from(sqrt_ratio_a_x64))) 208 | .div(&q64) 209 | .floor() 210 | .to_u128().unwrap() 211 | ) 212 | } 213 | } 214 | 215 | pub fn get_delta_amount_0_signed( 216 | sqrt_ratio_a_x64: u128, 217 | sqrt_ratio_b_x64: u128, 218 | liquidity: i128, 219 | ) -> Result { 220 | if liquidity < 0 { 221 | get_delta_amount_0_unsigned( 222 | sqrt_ratio_a_x64, 223 | sqrt_ratio_b_x64, 224 | u128::try_from(-liquidity).unwrap(), 225 | false, 226 | ) 227 | } else { 228 | get_delta_amount_0_unsigned( 229 | sqrt_ratio_a_x64, 230 | sqrt_ratio_b_x64, 231 | u128::try_from(liquidity).unwrap(), 232 | true, 233 | ) 234 | } 235 | } 236 | 237 | /// Helper function to get signed delta amount_1 for given liquidity and price range 238 | pub fn get_delta_amount_1_signed( 239 | sqrt_ratio_a_x64: u128, 240 | sqrt_ratio_b_x64: u128, 241 | liquidity: i128, 242 | ) -> Result { 243 | if liquidity < 0 { 244 | get_delta_amount_1_unsigned( 245 | sqrt_ratio_a_x64, 246 | sqrt_ratio_b_x64, 247 | u128::try_from(-liquidity).unwrap(), 248 | false, 249 | ) 250 | } else { 251 | get_delta_amount_1_unsigned( 252 | sqrt_ratio_a_x64, 253 | sqrt_ratio_b_x64, 254 | u128::try_from(liquidity).unwrap(), 255 | true, 256 | ) 257 | } 258 | } 259 | 260 | pub fn get_delta_amounts_signed( 261 | tick_current: i32, 262 | sqrt_price_x64_current: u128, 263 | tick_lower: i32, 264 | tick_upper: i32, 265 | liquidity_delta: i128, 266 | ) -> Result<(u64, u64), &'static str> { 267 | let mut amount_0 = 0; 268 | let mut amount_1 = 0; 269 | if tick_current < tick_lower { 270 | amount_0 = get_delta_amount_0_signed( 271 | get_sqrt_price_at_tick(tick_lower)?, 272 | get_sqrt_price_at_tick(tick_upper)?, 273 | liquidity_delta, 274 | ) 275 | .unwrap(); 276 | } else if tick_current < tick_upper { 277 | amount_0 = get_delta_amount_0_signed( 278 | sqrt_price_x64_current, 279 | get_sqrt_price_at_tick(tick_upper)?, 280 | liquidity_delta, 281 | ) 282 | .unwrap(); 283 | amount_1 = get_delta_amount_1_signed( 284 | get_sqrt_price_at_tick(tick_lower)?, 285 | sqrt_price_x64_current, 286 | liquidity_delta, 287 | ) 288 | .unwrap(); 289 | } else { 290 | amount_1 = get_delta_amount_1_signed( 291 | get_sqrt_price_at_tick(tick_lower)?, 292 | get_sqrt_price_at_tick(tick_upper)?, 293 | liquidity_delta, 294 | ) 295 | .unwrap(); 296 | } 297 | Ok((amount_0, amount_1)) 298 | } 299 | 300 | pub fn add_delta(x: u128, y: i128) -> Result { 301 | let z: u128; 302 | if y < 0 { 303 | z = x - u128::try_from(-y).unwrap(); 304 | if x <= z { 305 | return Err("liquidity sub value error") 306 | } 307 | } else { 308 | z = x + u128::try_from(y).unwrap(); 309 | if z < x { 310 | return Err("liquidity add value error") 311 | } 312 | } 313 | 314 | Ok(z) 315 | } 316 | 317 | #[cfg(test)] 318 | pub mod unit_test { 319 | 320 | } 321 | -------------------------------------------------------------------------------- /src/formula/clmm/raydium_tick_math.rs: -------------------------------------------------------------------------------- 1 | use crate::formula::clmm::u256_math::U128; 2 | 3 | /// The minimum tick 4 | pub const MIN_TICK: i32 = -443636; 5 | /// The minimum tick 6 | pub const MAX_TICK: i32 = -MIN_TICK; 7 | 8 | /// The minimum value that can be returned from #get_sqrt_price_at_tick. Equivalent to get_sqrt_price_at_tick(MIN_TICK) 9 | pub const MIN_SQRT_PRICE_X64: u128 = 4295048016; 10 | /// The maximum value that can be returned from #get_sqrt_price_at_tick. Equivalent to get_sqrt_price_at_tick(MAX_TICK) 11 | pub const MAX_SQRT_PRICE_X64: u128 = 79226673521066979257578248091; 12 | 13 | // Number 64, encoded as a U128 14 | const NUM_64: U128 = U128([64, 0]); 15 | 16 | const BIT_PRECISION: u32 = 16; 17 | 18 | /// Calculates 1.0001^(tick/2) as a U64.64 number representing 19 | /// the square root of the ratio of the two assets (token_1/token_0) 20 | /// 21 | /// Calculates result as a U64.64 22 | /// Each magic factor is `2^64 / (1.0001^(2^(i - 1)))` for i in `[0, 18)`. 23 | /// 24 | /// Throws if |tick| > MAX_TICK 25 | /// 26 | /// # Arguments 27 | /// * `tick` - Price tick 28 | /// 29 | pub fn get_sqrt_price_at_tick(tick: i32) -> Result { 30 | let abs_tick = tick.abs() as u32; 31 | // require!(abs_tick <= MAX_TICK as u32, ErrorCode::TickUpperOverflow); 32 | 33 | // i = 0 34 | let mut ratio = if abs_tick & 0x1 != 0 { 35 | U128([0xfffcb933bd6fb800, 0]) 36 | } else { 37 | // 2^64 38 | U128([0, 1]) 39 | }; 40 | // i = 1 41 | if abs_tick & 0x2 != 0 { 42 | ratio = (ratio * U128([0xfff97272373d4000, 0])) >> NUM_64 43 | }; 44 | // i = 2 45 | if abs_tick & 0x4 != 0 { 46 | ratio = (ratio * U128([0xfff2e50f5f657000, 0])) >> NUM_64 47 | }; 48 | // i = 3 49 | if abs_tick & 0x8 != 0 { 50 | ratio = (ratio * U128([0xffe5caca7e10f000, 0])) >> NUM_64 51 | }; 52 | // i = 4 53 | if abs_tick & 0x10 != 0 { 54 | ratio = (ratio * U128([0xffcb9843d60f7000, 0])) >> NUM_64 55 | }; 56 | // i = 5 57 | if abs_tick & 0x20 != 0 { 58 | ratio = (ratio * U128([0xff973b41fa98e800, 0])) >> NUM_64 59 | }; 60 | // i = 6 61 | if abs_tick & 0x40 != 0 { 62 | ratio = (ratio * U128([0xff2ea16466c9b000, 0])) >> NUM_64 63 | }; 64 | // i = 7 65 | if abs_tick & 0x80 != 0 { 66 | ratio = (ratio * U128([0xfe5dee046a9a3800, 0])) >> NUM_64 67 | }; 68 | // i = 8 69 | if abs_tick & 0x100 != 0 { 70 | ratio = (ratio * U128([0xfcbe86c7900bb000, 0])) >> NUM_64 71 | }; 72 | // i = 9 73 | if abs_tick & 0x200 != 0 { 74 | ratio = (ratio * U128([0xf987a7253ac65800, 0])) >> NUM_64 75 | }; 76 | // i = 10 77 | if abs_tick & 0x400 != 0 { 78 | ratio = (ratio * U128([0xf3392b0822bb6000, 0])) >> NUM_64 79 | }; 80 | // i = 11 81 | if abs_tick & 0x800 != 0 { 82 | ratio = (ratio * U128([0xe7159475a2caf000, 0])) >> NUM_64 83 | }; 84 | // i = 12 85 | if abs_tick & 0x1000 != 0 { 86 | ratio = (ratio * U128([0xd097f3bdfd2f2000, 0])) >> NUM_64 87 | }; 88 | // i = 13 89 | if abs_tick & 0x2000 != 0 { 90 | ratio = (ratio * U128([0xa9f746462d9f8000, 0])) >> NUM_64 91 | }; 92 | // i = 14 93 | if abs_tick & 0x4000 != 0 { 94 | ratio = (ratio * U128([0x70d869a156f31c00, 0])) >> NUM_64 95 | }; 96 | // i = 15 97 | if abs_tick & 0x8000 != 0 { 98 | ratio = (ratio * U128([0x31be135f97ed3200, 0])) >> NUM_64 99 | }; 100 | // i = 16 101 | if abs_tick & 0x10000 != 0 { 102 | ratio = (ratio * U128([0x9aa508b5b85a500, 0])) >> NUM_64 103 | }; 104 | // i = 17 105 | if abs_tick & 0x20000 != 0 { 106 | ratio = (ratio * U128([0x5d6af8dedc582c, 0])) >> NUM_64 107 | }; 108 | // i = 18 109 | if abs_tick & 0x40000 != 0 { 110 | ratio = (ratio * U128([0x2216e584f5fa, 0])) >> NUM_64 111 | } 112 | 113 | // Divide to obtain 1.0001^(2^(i - 1)) * 2^32 in numerator 114 | if tick > 0 { 115 | ratio = U128::MAX / ratio; 116 | } 117 | 118 | Ok(ratio.as_u128()) 119 | } 120 | 121 | /// Calculates the greatest tick value such that get_sqrt_price_at_tick(tick) <= ratio 122 | /// Throws if sqrt_price_x64 < MIN_SQRT_RATIO or sqrt_price_x64 > MAX_SQRT_RATIO 123 | /// 124 | /// Formula: `i = log base(√1.0001) (√P)` 125 | pub fn get_tick_at_sqrt_price(sqrt_price_x64: u128) -> Result { 126 | // second inequality must be < because the price can never reach the price at the max tick 127 | // require!( 128 | // sqrt_price_x64 >= MIN_SQRT_PRICE_X64 && sqrt_price_x64 < MAX_SQRT_PRICE_X64, 129 | // ErrorCode::SqrtPriceX64 130 | // ); 131 | 132 | // Determine log_b(sqrt_ratio). First by calculating integer portion (msb) 133 | let msb: u32 = 128 - sqrt_price_x64.leading_zeros() - 1; 134 | let log2p_integer_x32 = (msb as i128 - 64) << 32; 135 | 136 | // get fractional value (r/2^msb), msb always > 128 137 | // We begin the iteration from bit 63 (0.5 in Q64.64) 138 | let mut bit: i128 = 0x8000_0000_0000_0000i128; 139 | let mut precision = 0; 140 | let mut log2p_fraction_x64 = 0; 141 | 142 | // Log2 iterative approximation for the fractional part 143 | // Go through each 2^(j) bit where j < 64 in a Q64.64 number 144 | // Append current bit value to fraction result if r^2 Q2.126 is more than 2 145 | let mut r = if msb >= 64 { 146 | sqrt_price_x64 >> (msb - 63) 147 | } else { 148 | sqrt_price_x64 << (63 - msb) 149 | }; 150 | 151 | while bit > 0 && precision < BIT_PRECISION { 152 | r *= r; 153 | let is_r_more_than_two = r >> 127 as u32; 154 | r >>= 63 + is_r_more_than_two; 155 | log2p_fraction_x64 += bit * is_r_more_than_two as i128; 156 | bit >>= 1; 157 | precision += 1; 158 | } 159 | let log2p_fraction_x32 = log2p_fraction_x64 >> 32; 160 | let log2p_x32 = log2p_integer_x32 + log2p_fraction_x32; 161 | 162 | // 14 bit refinement gives an error margin of 2^-14 / log2 (√1.0001) = 0.8461 < 1 163 | // Since tick is a decimal, an error under 1 is acceptable 164 | 165 | // Change of base rule: multiply with 2^16 / log2 (√1.0001) 166 | let log_sqrt_10001_x64 = log2p_x32 * 59543866431248i128; 167 | 168 | // tick - 0.01 169 | let tick_low = ((log_sqrt_10001_x64 - 184467440737095516i128) >> 64) as i32; 170 | 171 | // tick + (2^-14 / log2(√1.001)) + 0.01 172 | let tick_high = ((log_sqrt_10001_x64 + 15793534762490258745i128) >> 64) as i32; 173 | 174 | Ok(if tick_low == tick_high { 175 | tick_low 176 | } else if get_sqrt_price_at_tick(tick_high).unwrap() <= sqrt_price_x64 { 177 | tick_high 178 | } else { 179 | tick_low 180 | }) 181 | } 182 | 183 | #[cfg(test)] 184 | mod tick_math_test { 185 | use super::*; 186 | mod get_sqrt_price_at_tick_test { 187 | use crate::formula::clmm::raydium_sqrt_price_math::Q64; 188 | use super::*; 189 | 190 | #[test] 191 | fn check_get_sqrt_price_at_tick_at_min_or_max_tick() { 192 | assert_eq!( 193 | get_sqrt_price_at_tick(MIN_TICK).unwrap(), 194 | MIN_SQRT_PRICE_X64 195 | ); 196 | let min_sqrt_price = MIN_SQRT_PRICE_X64 as f64 / Q64 as f64; 197 | println!("min_sqrt_price: {}", min_sqrt_price); 198 | assert_eq!( 199 | get_sqrt_price_at_tick(MAX_TICK).unwrap(), 200 | MAX_SQRT_PRICE_X64 201 | ); 202 | let max_sqrt_price = MAX_SQRT_PRICE_X64 as f64 / Q64 as f64; 203 | println!("max_sqrt_price: {}", max_sqrt_price); 204 | } 205 | } 206 | 207 | mod get_tick_at_sqrt_price_test { 208 | use super::*; 209 | 210 | #[test] 211 | fn test_sqrt_price_to_tick() { 212 | let sqrt_price_x64: Vec = vec![ 213 | 7182147241917313386, 214 | 7174399016327223095, 215 | 7174386368720733565, 216 | 7174388168782077692, 217 | 7174954712407921105, 218 | MAX_SQRT_PRICE_X64, 219 | MIN_SQRT_PRICE_X64, 220 | 40038806028187328673, 221 | 40040807918442726785, 222 | 40042809908790148078, 223 | 40044811999234571724, 224 | 40046814189781028227, 225 | 40048816480434496316, 226 | 40050818871200006995, 227 | 40052821362082540436, 228 | 40054823953087122549, 229 | 40056826644218744243, 230 | 40058829435482431925, 231 | 40060832326883166784, 232 | 40062835318425977113, 233 | 40064838410115852039, 234 | 40066841601957820377, 235 | 40068844893956864293, 236 | 40070848286118011711, 237 | 40072851778446255527, 238 | 40074855370946624180, 239 | 40076859063624100820, 240 | 40078862856483714286, 241 | 40080866749530458674, 242 | 40082870742769363323, 243 | 40084874836205412387, 244 | 40086879029843635811, 245 | 40088883323689028482, 246 | 40090887717746620836, 247 | 40092892212021398047, 248 | 40094896806518392439, 249 | 40096901501242597126, 250 | 40098906296199044947, 251 | 40100911191392722055, 252 | 40102916186828660400, 253 | 40104921282511856870, 254 | 40106926478447343933, 255 | 40108931774640108728, 256 | 40110937171095189807, 257 | 40112942667817573878, 258 | 40114948264812299995, 259 | 40116953962084356305, 260 | 40118959759638776770, 261 | 40120965657480560287, 262 | 40122971655614741310, 263 | 40124977754046309010, 264 | 40126983952780299724, 265 | 40128990251821710573, 266 | 40130996651175578421, 267 | 40133003150846893413, 268 | 40135009750840691521, 269 | 40137016451161973645 270 | ]; 271 | 272 | sqrt_price_x64.iter().for_each(|x| { 273 | println!("{}", get_tick_at_sqrt_price(*x).unwrap()) 274 | }) 275 | } 276 | 277 | #[test] 278 | fn test_tick_to_sqrt_price() { 279 | let mut a: Vec = (-15500i32..-15550i32).collect::>(); 280 | let mut b: Vec = (15500i32..15550i32).collect::>(); 281 | let mut tick: Vec = vec![]; 282 | tick.append(&mut a); 283 | tick.append(&mut b); 284 | 285 | tick.iter().for_each(|x| { 286 | println!("{}", get_sqrt_price_at_tick(*x).unwrap()) 287 | }) 288 | } 289 | 290 | #[test] 291 | fn check_get_tick_at_sqrt_price_at_min_or_max_sqrt_price() { 292 | assert_eq!( 293 | get_tick_at_sqrt_price(MIN_SQRT_PRICE_X64).unwrap(), 294 | MIN_TICK, 295 | ); 296 | 297 | // we can't reach MAX_SQRT_PRICE_X64 298 | assert_eq!( 299 | get_tick_at_sqrt_price(MAX_SQRT_PRICE_X64 - 1).unwrap(), 300 | MAX_TICK - 1, 301 | ); 302 | } 303 | } 304 | 305 | #[test] 306 | fn test_sqrt_price_from_tick_index_at_max() { 307 | let r = get_tick_at_sqrt_price(MAX_SQRT_PRICE_X64).unwrap(); 308 | assert_eq!(&r, &MAX_TICK); 309 | } 310 | 311 | #[test] 312 | fn test_sqrt_price_from_tick_index_at_max_sub_one() { 313 | let sqrt_price_x64 = MAX_SQRT_PRICE_X64 - 1; 314 | let r = get_tick_at_sqrt_price(sqrt_price_x64).unwrap(); 315 | assert_eq!(&r, &(MAX_TICK - 1)); 316 | } 317 | 318 | #[test] 319 | fn tick_round_down() { 320 | // tick is negative 321 | let sqrt_price_x64 = get_sqrt_price_at_tick(-28861).unwrap(); 322 | let mut tick = get_tick_at_sqrt_price(sqrt_price_x64).unwrap(); 323 | assert_eq!(tick, -28861); 324 | tick = get_tick_at_sqrt_price(sqrt_price_x64 + 1).unwrap(); 325 | assert_eq!(tick, -28861); 326 | tick = get_tick_at_sqrt_price(get_sqrt_price_at_tick(-28860).unwrap() - 1).unwrap(); 327 | assert_eq!(tick, -28861); 328 | tick = get_tick_at_sqrt_price(sqrt_price_x64 - 1).unwrap(); 329 | assert_eq!(tick, -28862); 330 | 331 | // tick is positive 332 | let sqrt_price_x64 = get_sqrt_price_at_tick(28861).unwrap(); 333 | tick = get_tick_at_sqrt_price(sqrt_price_x64).unwrap(); 334 | assert_eq!(tick, 28861); 335 | tick = get_tick_at_sqrt_price(sqrt_price_x64 + 1).unwrap(); 336 | assert_eq!(tick, 28861); 337 | tick = get_tick_at_sqrt_price(get_sqrt_price_at_tick(28862).unwrap() - 1).unwrap(); 338 | assert_eq!(tick, 28861); 339 | tick = get_tick_at_sqrt_price(sqrt_price_x64 - 1).unwrap(); 340 | assert_eq!(tick, 28860); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/formula/clmm/test/complex_swap_test.rs: -------------------------------------------------------------------------------- 1 | /* 2 | this test is to verify multiple pools swap that have same formula return same result 3 | e.g. 4 | */ 5 | 6 | #[cfg(test)] 7 | mod complex_test { 8 | use crate::formula::clmm::orca_swap_state::{PostSwapUpdate, SwapTickSequence}; 9 | use crate::formula::clmm::test::raydium_swap_test::swap_test::{build_swap_param, build_tick, get_tick_array_states_mut, TickArrayInfo}; 10 | use crate::formula::clmm::test::swap_test_fixture::{SwapTestFixture, SwapTestFixtureInfo, TS_128}; 11 | use crate::formula::raydium_clmm::swap_internal; 12 | 13 | pub fn default_orca_swap( 14 | liquidity: u128, 15 | tick_current: i32, 16 | start_tick_index: i32, 17 | amount: u64, 18 | sqrt_price_x64_limit: u128, 19 | a_to_b: bool, 20 | amount_specified_is_input: bool, 21 | ) -> PostSwapUpdate { 22 | let swap_test_info = SwapTestFixture::new(SwapTestFixtureInfo { 23 | tick_spacing: TS_128, 24 | liquidity, 25 | curr_tick_index: tick_current, 26 | start_tick_index, 27 | trade_amount: amount, 28 | sqrt_price_limit: sqrt_price_x64_limit, 29 | amount_specified_is_input, 30 | a_to_b, 31 | ..Default::default() 32 | }); 33 | let mut tick_sequence = SwapTickSequence::new( 34 | swap_test_info.tick_arrays[0].to_owned(), 35 | Some(swap_test_info.tick_arrays[1].to_owned()), 36 | Some(swap_test_info.tick_arrays[2].to_owned()), 37 | ); 38 | let post_swap = swap_test_info.run(&mut tick_sequence, 100); 39 | 40 | post_swap 41 | } 42 | 43 | #[test] 44 | pub fn test_a() { 45 | let mut tick_current = -32395; 46 | let mut start_tick_index = vec![-32400, -36000]; 47 | let mut liquidity = 5124165121219; 48 | let mut sqrt_price_x64 = 3651942632306380802; 49 | let sqrt_price_x64_limit = 3049500711113990606u128; 50 | let mut amount = 12188240002u64; 51 | 52 | let (amm_config, pool_state, mut tick_array_states) = 53 | build_swap_param( 54 | tick_current, 55 | 60, 56 | sqrt_price_x64, 57 | liquidity, 58 | vec![ 59 | TickArrayInfo { 60 | start_tick_index: start_tick_index[0], 61 | ticks: vec![ 62 | build_tick(-32400, 277065331032, -277065331032).take(), 63 | build_tick(-29220, 1330680689, -1330680689).take(), 64 | build_tick(-28860, 6408486554, -6408486554).take(), 65 | ], 66 | }, 67 | TickArrayInfo { 68 | start_tick_index: start_tick_index[1], 69 | ticks: vec![ 70 | build_tick(-32460, 1194569667438, 536061033698).take(), 71 | build_tick(-32520, 790917615645, 790917615645).take(), 72 | build_tick(-32580, 152146472301, 128451145459).take(), 73 | build_tick(-32640, 2625605835354, -1492054447712).take(), 74 | ], 75 | }, 76 | ], 77 | ); 78 | 79 | let (amount_0, amount_1) = swap_internal( 80 | &amm_config, 81 | &mut pool_state.borrow_mut(), 82 | &mut get_tick_array_states_mut(&tick_array_states), 83 | &None, 84 | amount, 85 | sqrt_price_x64_limit, 86 | true, 87 | true, 88 | ).unwrap(); 89 | 90 | let post_swap = default_orca_swap( 91 | liquidity, 92 | tick_current, 93 | start_tick_index[0], 94 | amount, 95 | sqrt_price_x64_limit, 96 | true, 97 | true 98 | ); 99 | 100 | println!("r1(raydium): {}, {}", amount_0, amount_1); 101 | println!("r1(orca): {}, {}", post_swap.amount_a, post_swap.amount_b); 102 | 103 | //////////////////////////////////////////////////// 104 | tick_current = pool_state.borrow().tick_current; 105 | sqrt_price_x64 = pool_state.borrow().sqrt_price_x64; 106 | liquidity = pool_state.borrow().liquidity; 107 | amount = 121882400020; 108 | 109 | tick_array_states.pop_front(); 110 | let (amount_0, amount_1) = swap_internal( 111 | &amm_config, 112 | &mut pool_state.borrow_mut(), 113 | &mut get_tick_array_states_mut(&tick_array_states), 114 | &None, 115 | amount, 116 | sqrt_price_x64_limit, 117 | true, 118 | true, 119 | ) 120 | .unwrap(); 121 | 122 | let post_swap = default_orca_swap( 123 | liquidity, 124 | tick_current, 125 | start_tick_index[1], 126 | amount, 127 | sqrt_price_x64_limit, 128 | true, 129 | true 130 | ); 131 | 132 | println!("r2(raydium): {}, {}", amount_0, amount_1); 133 | println!("r2(orca): {}, {}", post_swap.amount_a, post_swap.amount_b); 134 | 135 | //////////////////////////////////////////////////// 136 | tick_current = pool_state.borrow().tick_current; 137 | sqrt_price_x64 = pool_state.borrow().sqrt_price_x64; 138 | liquidity = pool_state.borrow().liquidity; 139 | amount = 60941200010; 140 | 141 | let (amount_0, amount_1) = swap_internal( 142 | &amm_config, 143 | &mut pool_state.borrow_mut(), 144 | &mut get_tick_array_states_mut(&tick_array_states), 145 | &None, 146 | amount, 147 | sqrt_price_x64_limit, 148 | true, 149 | true, 150 | ) 151 | .unwrap(); 152 | 153 | let post_swap = default_orca_swap( 154 | liquidity, 155 | tick_current, 156 | start_tick_index[1], 157 | amount, 158 | sqrt_price_x64_limit, 159 | true, 160 | true 161 | ); 162 | 163 | println!("r3(raydium): {}, {}", amount_0, amount_1); 164 | println!("r3(orca): {}, {}", post_swap.amount_a, post_swap.amount_b); 165 | } 166 | 167 | #[test] 168 | pub fn test_b() { 169 | let mut tick_current = -32395; 170 | let mut liquidity = 5124165121219; 171 | let mut sqrt_price_x64 = 3651942632306380802; 172 | 173 | let mut start_tick_index = vec![-36000]; 174 | let sqrt_price_x64_limit = 3049500711113990606u128; 175 | let amount = 477470480u64; 176 | 177 | let (amm_config, pool_state, mut tick_array_states) = 178 | build_swap_param( 179 | tick_current, 180 | 60, 181 | sqrt_price_x64, 182 | liquidity, 183 | vec![ 184 | TickArrayInfo { 185 | start_tick_index: -32400, 186 | ticks: vec![ 187 | build_tick(-32400, 277065331032, -277065331032).take(), 188 | build_tick(-29220, 1330680689, -1330680689).take(), 189 | build_tick(-28860, 6408486554, -6408486554).take(), 190 | ], 191 | }, 192 | TickArrayInfo { 193 | start_tick_index: -36000, 194 | ticks: vec![ 195 | build_tick(-32460, 1194569667438, 536061033698).take(), 196 | build_tick(-32520, 790917615645, 790917615645).take(), 197 | build_tick(-32580, 152146472301, 128451145459).take(), 198 | build_tick(-32640, 2625605835354, -1492054447712).take(), 199 | ], 200 | }, 201 | ], 202 | ); 203 | 204 | // just cross the tickarray boundary(-32400), hasn't reached the next tick array initialized tick 205 | let (amount_0, amount_1) = swap_internal( 206 | &amm_config, 207 | &mut pool_state.borrow_mut(), 208 | &mut get_tick_array_states_mut(&tick_array_states), 209 | &None, 210 | amount, 211 | sqrt_price_x64_limit, 212 | true, 213 | false, 214 | ).unwrap(); 215 | 216 | let post_swap = default_orca_swap( 217 | liquidity, 218 | tick_current, 219 | start_tick_index[0], 220 | amount, 221 | sqrt_price_x64_limit, 222 | true, 223 | false, 224 | ); 225 | 226 | println!("raydium: {}, {}", amount_1, amount_0); 227 | println!("orca: {}, {}", post_swap.amount_b, post_swap.amount_a); 228 | } 229 | } -------------------------------------------------------------------------------- /src/formula/clmm/test/liquidity_test_fixture.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey::Pubkey; 2 | use crate::formula::clmm::orca_swap_state::{NUM_REWARDS, Q64_RESOLUTION, Tick, TickUpdate}; 3 | use crate::formula::clmm::orca_swap_state::tick_builder::TickBuilder; 4 | use crate::formula::clmm::raydium_swap_state::add_delta; 5 | use crate::r#struct::pools::{OrcaClmmMarket, WhirlpoolRewardInfo}; 6 | use crate::r#struct::pools::whirlpool_builder::WhirlpoolBuilder; 7 | 8 | // const BELOW_LOWER_TICK_INDEX: i32 = -120; 9 | // const ABOVE_UPPER_TICK_INDEX: i32 = 120; 10 | // 11 | // pub enum CurrIndex { 12 | // Below, 13 | // Inside, 14 | // Above, 15 | // } 16 | // 17 | // pub enum TickLabel { 18 | // Upper, 19 | // Lower, 20 | // } 21 | // 22 | // pub enum Direction { 23 | // Left, 24 | // Right, 25 | // } 26 | // 27 | // // State for testing modifying liquidity in a single whirlpool position 28 | // pub struct LiquidityTestFixture { 29 | // pub whirlpool: OrcaClmmMarket, 30 | // // pub position: Position, 31 | // pub tick_lower: Tick, 32 | // pub tick_upper: Tick, 33 | // } 34 | // 35 | // pub struct LiquidityTestFixtureInfo { 36 | // pub curr_index_loc: CurrIndex, 37 | // pub whirlpool_liquidity: u128, 38 | // pub position_liquidity: u128, 39 | // pub tick_lower_liquidity_gross: u128, 40 | // pub tick_upper_liquidity_gross: u128, 41 | // pub fee_growth_global_a: u128, 42 | // pub fee_growth_global_b: u128, 43 | // pub reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS], 44 | // } 45 | // 46 | // impl LiquidityTestFixture { 47 | // pub fn new(info: LiquidityTestFixtureInfo) -> LiquidityTestFixture { 48 | // assert!(info.tick_lower_liquidity_gross < i64::MAX as u128); 49 | // assert!(info.tick_upper_liquidity_gross < i64::MAX as u128); 50 | // 51 | // // Tick's must have enough at least enough liquidity to support the position 52 | // assert!(info.tick_lower_liquidity_gross >= info.position_liquidity); 53 | // assert!(info.tick_upper_liquidity_gross >= info.position_liquidity); 54 | // 55 | // let curr_index = match info.curr_index_loc { 56 | // CurrIndex::Below => BELOW_LOWER_TICK_INDEX, 57 | // CurrIndex::Inside => 0, 58 | // CurrIndex::Above => ABOVE_UPPER_TICK_INDEX, 59 | // }; 60 | // 61 | // let whirlpool = WhirlpoolBuilder::new() 62 | // .tick_current_index(curr_index) 63 | // .liquidity(info.whirlpool_liquidity) 64 | // .reward_infos(info.reward_infos) 65 | // .fee_growth_global_a(info.fee_growth_global_a) 66 | // .fee_growth_global_b(info.fee_growth_global_b) 67 | // .build(); 68 | // 69 | // let tick_lower_initialized = info.tick_lower_liquidity_gross > 0; 70 | // let tick_upper_initialized = info.tick_upper_liquidity_gross > 0; 71 | // 72 | // LiquidityTestFixture { 73 | // whirlpool, 74 | // // position: PositionBuilder::new(-100, 100) 75 | // // .liquidity(info.position_liquidity) 76 | // // .build(), 77 | // tick_lower: TickBuilder::default() 78 | // .initialized(tick_lower_initialized) 79 | // .liquidity_gross(info.tick_lower_liquidity_gross) 80 | // .liquidity_net(info.tick_lower_liquidity_gross as i128) 81 | // .build(), 82 | // tick_upper: TickBuilder::default() 83 | // .initialized(tick_upper_initialized) 84 | // .liquidity_gross(info.tick_upper_liquidity_gross) 85 | // .liquidity_net(-(info.tick_upper_liquidity_gross as i128)) 86 | // .build(), 87 | // } 88 | // } 89 | // 90 | // pub fn increment_whirlpool_fee_growths( 91 | // &mut self, 92 | // fee_growth_delta_a: u128, 93 | // fee_growth_delta_b: u128, 94 | // ) { 95 | // self.whirlpool.fee_growth_global_a = self 96 | // .whirlpool 97 | // .fee_growth_global_a 98 | // .wrapping_add(fee_growth_delta_a); 99 | // self.whirlpool.fee_growth_global_b = self 100 | // .whirlpool 101 | // .fee_growth_global_b 102 | // .wrapping_add(fee_growth_delta_b); 103 | // } 104 | // 105 | // pub fn increment_whirlpool_reward_growths_by_time(&mut self, seconds: u64) { 106 | // let next_timestamp = self.whirlpool.reward_last_updated_timestamp + seconds; 107 | // self.whirlpool.reward_infos = 108 | // next_whirlpool_reward_infos(&self.whirlpool, next_timestamp).unwrap(); 109 | // self.whirlpool.reward_last_updated_timestamp = next_timestamp; 110 | // } 111 | // 112 | // /// Simulates crossing a tick within the test fixture. 113 | // pub fn cross_tick(&mut self, tick_label: TickLabel, direction: Direction) { 114 | // let tick = match tick_label { 115 | // TickLabel::Lower => &mut self.tick_lower, 116 | // TickLabel::Upper => &mut self.tick_upper, 117 | // }; 118 | // let update = next_tick_cross_update( 119 | // tick, 120 | // self.whirlpool.fee_growth_global_a, 121 | // self.whirlpool.fee_growth_global_b, 122 | // &self.whirlpool.reward_infos, 123 | // ) 124 | // .unwrap(); 125 | // 126 | // tick.update(&update); 127 | // 128 | // // self.whirlpool.liquidity = add_liquidity_delta( 129 | // self.whirlpool.liquidity = add_delta( 130 | // self.whirlpool.liquidity, 131 | // match direction { 132 | // Direction::Left => -tick.liquidity_net, 133 | // Direction::Right => tick.liquidity_net, 134 | // }, 135 | // ) 136 | // .unwrap(); 137 | // 138 | // match tick_label { 139 | // TickLabel::Lower => match direction { 140 | // Direction::Right => self.whirlpool.tick_current_index = 0, 141 | // Direction::Left => self.whirlpool.tick_current_index = BELOW_LOWER_TICK_INDEX, 142 | // }, 143 | // TickLabel::Upper => match direction { 144 | // Direction::Left => self.whirlpool.tick_current_index = 0, 145 | // Direction::Right => self.whirlpool.tick_current_index = ABOVE_UPPER_TICK_INDEX, 146 | // }, 147 | // } 148 | // } 149 | // 150 | // pub fn apply_update( 151 | // &mut self, 152 | // update: &ModifyLiquidityUpdate, 153 | // reward_last_updated_timestamp: u64, 154 | // ) { 155 | // assert!(reward_last_updated_timestamp >= self.whirlpool.reward_last_updated_timestamp); 156 | // self.whirlpool.reward_last_updated_timestamp = reward_last_updated_timestamp; 157 | // self.whirlpool.liquidity = update.whirlpool_liquidity; 158 | // self.whirlpool.reward_infos = update.reward_infos; 159 | // self.tick_lower.update(&update.tick_lower_update); 160 | // self.tick_upper.update(&update.tick_upper_update); 161 | // // self.position.update(&update.position_update); 162 | // } 163 | // } 164 | // 165 | // pub fn create_whirlpool_reward_infos( 166 | // emissions_per_second_x64: u128, 167 | // growth_global_x64: u128, 168 | // ) -> [WhirlpoolRewardInfo; NUM_REWARDS] { 169 | // [ 170 | // WhirlpoolRewardInfo { 171 | // mint: Pubkey::new_unique(), 172 | // emissions_per_second_x64, 173 | // growth_global_x64, 174 | // ..Default::default() 175 | // }, 176 | // WhirlpoolRewardInfo { 177 | // mint: Pubkey::new_unique(), 178 | // emissions_per_second_x64, 179 | // growth_global_x64, 180 | // ..Default::default() 181 | // }, 182 | // WhirlpoolRewardInfo { 183 | // mint: Pubkey::new_unique(), 184 | // emissions_per_second_x64, 185 | // growth_global_x64, 186 | // ..Default::default() 187 | // }, 188 | // ] 189 | // } 190 | // 191 | // // pub fn create_position_reward_infos( 192 | // // growth_inside_checkpoint: u128, 193 | // // amount_owed: u64, 194 | // // ) -> [PositionRewardInfo; NUM_REWARDS] { 195 | // // [ 196 | // // PositionRewardInfo { 197 | // // growth_inside_checkpoint, 198 | // // amount_owed, 199 | // // }, 200 | // // PositionRewardInfo { 201 | // // growth_inside_checkpoint, 202 | // // amount_owed, 203 | // // }, 204 | // // PositionRewardInfo { 205 | // // growth_inside_checkpoint, 206 | // // amount_owed, 207 | // // }, 208 | // // ] 209 | // // } 210 | // 211 | // pub fn create_reward_growths(growth_global_x64: u128) -> [u128; NUM_REWARDS] { 212 | // [growth_global_x64, growth_global_x64, growth_global_x64] 213 | // } 214 | // 215 | // pub fn to_x64(n: u128) -> u128 { 216 | // n << Q64_RESOLUTION 217 | // } 218 | // 219 | // pub fn assert_whirlpool_reward_growths( 220 | // reward_infos: &[WhirlpoolRewardInfo; NUM_REWARDS], 221 | // expected_growth: u128, 222 | // ) { 223 | // assert_eq!( 224 | // WhirlpoolRewardInfo::to_reward_growths(reward_infos), 225 | // create_reward_growths(expected_growth) 226 | // ) 227 | // } 228 | // 229 | // pub struct ModifyLiquidityExpectation { 230 | // pub whirlpool_liquidity: u128, 231 | // pub whirlpool_reward_growths: [u128; NUM_REWARDS], 232 | // // pub position_update: PositionUpdate, 233 | // pub tick_lower_update: TickUpdate, 234 | // pub tick_upper_update: TickUpdate, 235 | // } 236 | // 237 | // pub fn assert_modify_liquidity( 238 | // update: &ModifyLiquidityUpdate, 239 | // expect: &ModifyLiquidityExpectation, 240 | // ) { 241 | // assert_eq!(update.whirlpool_liquidity, expect.whirlpool_liquidity); 242 | // assert_eq!( 243 | // WhirlpoolRewardInfo::to_reward_growths(&update.reward_infos), 244 | // expect.whirlpool_reward_growths 245 | // ); 246 | // assert_eq!(update.tick_lower_update, expect.tick_lower_update); 247 | // assert_eq!(update.tick_upper_update, expect.tick_upper_update); 248 | // // assert_eq!(update.position_update, expect.position_update); 249 | // } 250 | 251 | pub fn create_whirlpool_reward_infos( 252 | emissions_per_second_x64: u128, 253 | growth_global_x64: u128, 254 | ) -> [WhirlpoolRewardInfo; NUM_REWARDS] { 255 | [ 256 | WhirlpoolRewardInfo { 257 | mint: Pubkey::new_unique(), 258 | emissions_per_second_x64, 259 | growth_global_x64, 260 | ..Default::default() 261 | }, 262 | WhirlpoolRewardInfo { 263 | mint: Pubkey::new_unique(), 264 | emissions_per_second_x64, 265 | growth_global_x64, 266 | ..Default::default() 267 | }, 268 | WhirlpoolRewardInfo { 269 | mint: Pubkey::new_unique(), 270 | emissions_per_second_x64, 271 | growth_global_x64, 272 | ..Default::default() 273 | }, 274 | ] 275 | } -------------------------------------------------------------------------------- /src/formula/clmm/test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod swap_test_fixture; 2 | pub mod liquidity_test_fixture; 3 | mod raydium_swap_test; 4 | mod orca_swap_test; 5 | mod complex_swap_test; -------------------------------------------------------------------------------- /src/formula/clmm/test/swap_test_fixture.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey::Pubkey; 2 | 3 | use crate::formula::clmm::orca_swap_state::{MAX_TICK_INDEX, MIN_TICK_INDEX, NUM_REWARDS, PostSwapUpdate, SwapTickSequence, Tick, TICK_ARRAY_SIZE, TICK_ARRAY_SIZE_USIZE, TickArray, TickArrayType, TickUpdate}; 4 | use crate::formula::clmm::orca_swap_state::tick_builder::TickBuilder; 5 | use crate::formula::clmm::raydium_sqrt_price_math::tick_to_sqrt_price_x64; 6 | use crate::formula::orca_clmm::swap_internal; 7 | use crate::r#struct::pools::{OrcaClmmMarket, WhirlpoolRewardInfo}; 8 | use crate::r#struct::pools::whirlpool_builder::WhirlpoolBuilder; 9 | 10 | pub const TS_8: u16 = 8; 11 | pub const TS_128: u16 = 128; 12 | 13 | const NO_TICKS_VEC: &Vec = &vec![]; 14 | 15 | pub struct SwapTestFixture { 16 | pub whirlpool: OrcaClmmMarket, 17 | pub tick_arrays: Vec, 18 | pub trade_amount: u64, 19 | pub sqrt_price_limit: u128, 20 | pub amount_specified_is_input: bool, 21 | pub a_to_b: bool, 22 | pub reward_last_updated_timestamp: u64, 23 | } 24 | 25 | #[derive(Default)] 26 | pub struct TestTickInfo { 27 | pub index: i32, 28 | pub liquidity_net: i128, 29 | pub fee_growth_outside_a: u128, 30 | pub fee_growth_outside_b: u128, 31 | pub reward_growths_outside: [u128; NUM_REWARDS], 32 | } 33 | 34 | pub struct SwapTestFixtureInfo<'info> { 35 | pub tick_spacing: u16, 36 | pub liquidity: u128, 37 | pub curr_tick_index: i32, 38 | pub start_tick_index: i32, 39 | pub trade_amount: u64, 40 | pub sqrt_price_limit: u128, 41 | pub amount_specified_is_input: bool, 42 | pub a_to_b: bool, 43 | pub reward_last_updated_timestamp: u64, 44 | pub reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS], 45 | pub fee_growth_global_a: u128, 46 | pub fee_growth_global_b: u128, 47 | pub array_1_ticks: &'info Vec, 48 | pub array_2_ticks: Option<&'info Vec>, 49 | pub array_3_ticks: Option<&'info Vec>, 50 | pub fee_rate: u16, 51 | pub protocol_fee_rate: u16, 52 | } 53 | 54 | impl<'info> Default for SwapTestFixtureInfo<'info> { 55 | fn default() -> Self { 56 | SwapTestFixtureInfo { 57 | tick_spacing: TS_128, 58 | liquidity: 0, 59 | curr_tick_index: 0, 60 | start_tick_index: 0, 61 | trade_amount: 0, 62 | sqrt_price_limit: 0, 63 | amount_specified_is_input: false, 64 | a_to_b: false, 65 | reward_last_updated_timestamp: 0, 66 | reward_infos: [ 67 | WhirlpoolRewardInfo::default(), 68 | WhirlpoolRewardInfo::default(), 69 | WhirlpoolRewardInfo::default(), 70 | ], 71 | fee_growth_global_a: 0, 72 | fee_growth_global_b: 0, 73 | array_1_ticks: NO_TICKS_VEC, 74 | array_2_ticks: None, 75 | array_3_ticks: None, 76 | fee_rate: 0, 77 | protocol_fee_rate: 0, 78 | } 79 | } 80 | } 81 | 82 | pub struct SwapTestExpectation { 83 | pub traded_amount_a: u64, 84 | pub traded_amount_b: u64, 85 | pub end_tick_index: i32, 86 | pub end_liquidity: u128, 87 | pub end_reward_growths: [u128; NUM_REWARDS], 88 | } 89 | 90 | #[derive(Default)] 91 | pub struct TickExpectation { 92 | pub fee_growth_outside_a: u128, 93 | pub fee_growth_outside_b: u128, 94 | pub reward_growths_outside: [u128; NUM_REWARDS], 95 | } 96 | 97 | pub fn assert_swap(swap_update: &PostSwapUpdate, expect: &SwapTestExpectation) { 98 | assert_eq!(swap_update.amount_a, expect.traded_amount_a); 99 | assert_eq!(swap_update.amount_b, expect.traded_amount_b); 100 | assert_eq!(swap_update.next_tick_index, expect.end_tick_index); 101 | assert_eq!(swap_update.next_liquidity, expect.end_liquidity); 102 | // assert_eq!( 103 | // WhirlpoolRewardInfo::to_reward_growths(&swap_update.next_reward_infos), 104 | // expect.end_reward_growths 105 | // ); 106 | } 107 | 108 | pub fn assert_swap_tick_state(tick: &Tick, expect: &TickExpectation) { 109 | assert_eq!({ tick.fee_growth_outside_a }, expect.fee_growth_outside_a); 110 | assert_eq!({ tick.fee_growth_outside_b }, expect.fee_growth_outside_b); 111 | assert_eq!( 112 | { tick.reward_growths_outside }, 113 | expect.reward_growths_outside 114 | ); 115 | } 116 | 117 | pub fn build_filled_tick_array(start_index: i32, tick_spacing: u16) -> Vec { 118 | let mut array_ticks: Vec = vec![]; 119 | for n in 0..TICK_ARRAY_SIZE { 120 | let index = start_index + n * tick_spacing as i32; 121 | if (MIN_TICK_INDEX..MAX_TICK_INDEX).contains(&index) { 122 | array_ticks.push(TestTickInfo { 123 | index, 124 | liquidity_net: -5, 125 | ..Default::default() 126 | }); 127 | } 128 | } 129 | array_ticks 130 | } 131 | 132 | impl SwapTestFixture { 133 | pub fn new(info: SwapTestFixtureInfo) -> SwapTestFixture { 134 | let whirlpool = WhirlpoolBuilder::new() 135 | .liquidity(info.liquidity) 136 | // todo 137 | .sqrt_price(tick_to_sqrt_price_x64(&info.curr_tick_index).unwrap()) 138 | .tick_spacing(info.tick_spacing) 139 | .tick_current_index(info.curr_tick_index) 140 | .reward_last_updated_timestamp(info.reward_last_updated_timestamp) 141 | .reward_infos(info.reward_infos) 142 | .fee_growth_global_a(info.fee_growth_global_a) 143 | .fee_growth_global_b(info.fee_growth_global_b) 144 | .fee_rate(info.fee_rate) 145 | .protocol_fee_rate(info.protocol_fee_rate) 146 | .build(); 147 | 148 | let array_ticks: Vec>> = vec![ 149 | Some(info.array_1_ticks), 150 | info.array_2_ticks, 151 | info.array_3_ticks, 152 | ]; 153 | 154 | let mut ref_mut_tick_arrays = Vec::with_capacity(3); 155 | let direction: i32 = if info.a_to_b { -1 } else { 1 }; 156 | 157 | for (i, array) in array_ticks.iter().enumerate() { 158 | let array_index = ::from(i as u16); 159 | let array_start_tick_index = info.start_tick_index 160 | + info.tick_spacing as i32 * TICK_ARRAY_SIZE * array_index * direction; 161 | 162 | let mut new_ta = TickArray { 163 | start_tick_index: array_start_tick_index, 164 | ticks: [Tick::default(); TICK_ARRAY_SIZE_USIZE], 165 | whirlpool: Pubkey::default(), 166 | }; 167 | 168 | if array.is_none() { 169 | ref_mut_tick_arrays.push(new_ta); 170 | continue; 171 | } 172 | 173 | let tick_array = array.unwrap(); 174 | 175 | for tick in tick_array { 176 | let update = TickUpdate::from( 177 | &TickBuilder::default() 178 | .initialized(true) 179 | .liquidity_net(tick.liquidity_net) 180 | .fee_growth_outside_a(tick.fee_growth_outside_a) 181 | .fee_growth_outside_b(tick.fee_growth_outside_b) 182 | .reward_growths_outside(tick.reward_growths_outside) 183 | .build(), 184 | ); 185 | let update_result = new_ta.update_tick(tick.index, info.tick_spacing, &update); 186 | if update_result.is_err() { 187 | panic!("Failed to set tick {}", tick.index); 188 | } 189 | } 190 | 191 | ref_mut_tick_arrays.push(new_ta); 192 | } 193 | 194 | SwapTestFixture { 195 | whirlpool, 196 | tick_arrays: ref_mut_tick_arrays, 197 | 198 | trade_amount: info.trade_amount, 199 | sqrt_price_limit: info.sqrt_price_limit, 200 | amount_specified_is_input: info.amount_specified_is_input, 201 | a_to_b: info.a_to_b, 202 | reward_last_updated_timestamp: info.reward_last_updated_timestamp, 203 | } 204 | } 205 | 206 | pub fn run(&self, tick_sequence: &mut SwapTickSequence, next_timestamp: u64) -> PostSwapUpdate { 207 | swap_internal( 208 | &self.whirlpool, 209 | tick_sequence, 210 | self.trade_amount, 211 | self.sqrt_price_limit, 212 | self.amount_specified_is_input, 213 | self.a_to_b, 214 | next_timestamp, 215 | ) 216 | .unwrap() 217 | } 218 | 219 | pub fn eval( 220 | &self, 221 | tick_sequence: &mut SwapTickSequence, 222 | next_timestamp: u64, 223 | ) -> Result { 224 | swap_internal( 225 | &self.whirlpool, 226 | tick_sequence, 227 | self.trade_amount, 228 | self.sqrt_price_limit, 229 | self.amount_specified_is_input, 230 | self.a_to_b, 231 | next_timestamp, 232 | ) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/formula/constant_product.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, Mul, Sub}; 2 | use num_bigfloat::BigFloat; 3 | 4 | // not used 5 | pub struct DefaultConstantProduct { 6 | pub token_a_amount: u64, 7 | pub token_b_amount: u64, 8 | pub decimal_diff: i32, 9 | pub swap_fee_numerator: u64, 10 | pub swap_fee_denominator: u64 11 | } 12 | 13 | impl ConstantProductBase for DefaultConstantProduct { 14 | fn calculate_fee(&self, amount_in: u64) -> BigFloat { 15 | BigFloat::from(self.swap_fee_numerator) / BigFloat::from(self.swap_fee_denominator) * BigFloat::from(amount_in) 16 | } 17 | 18 | fn calculate_liquidity(&self) -> u128 { 19 | u128::from(self.token_a_amount) * u128::from(self.token_b_amount) 20 | } 21 | 22 | fn swap(&self, amount_in: u64, zero_for_one: bool) -> BigFloat { 23 | let fee = BigFloat::from(self.calculate_fee(amount_in)); 24 | 25 | let amount_in_with_fee = BigFloat::from(amount_in).sub(fee); 26 | let amount_out = (BigFloat::from(self.token_b_amount).mul(amount_in_with_fee)) / (BigFloat::from(self.token_a_amount).add(amount_in_with_fee)); 27 | 28 | amount_out 29 | } 30 | } 31 | 32 | pub trait ConstantProductBase { 33 | fn calculate_fee(&self, amount_in: u64) -> BigFloat; 34 | fn calculate_liquidity(&self) -> u128; 35 | fn swap(&self, amount_in: u64, zero_for_one: bool) -> BigFloat; 36 | } -------------------------------------------------------------------------------- /src/formula/dlmm/bin_array_bitmap_extension.rs: -------------------------------------------------------------------------------- 1 | use ruint::aliases::U512; 2 | use std::ops::BitXor; 3 | use solana_sdk::pubkey::Pubkey; 4 | use crate::formula::dlmm::constant::{BIN_ARRAY_BITMAP_SIZE, EXTENSION_BIN_ARRAY_BITMAP_SIZE}; 5 | use crate::formula::dlmm::safe_math::SafeMath; 6 | use crate::formula::dlmm::utils_math::one; 7 | 8 | #[derive(Debug)] 9 | pub struct BinArrayBitmapExtension { 10 | pub lb_pair: Pubkey, 11 | /// Packed initialized bin array state for start_bin_index is positive 12 | pub positive_bin_array_bitmap: [[u64; 8]; EXTENSION_BIN_ARRAY_BITMAP_SIZE], 13 | /// Packed initialized bin array state for start_bin_index is negative 14 | pub negative_bin_array_bitmap: [[u64; 8]; EXTENSION_BIN_ARRAY_BITMAP_SIZE], 15 | } 16 | 17 | impl Default for BinArrayBitmapExtension { 18 | #[inline] 19 | fn default() -> BinArrayBitmapExtension { 20 | BinArrayBitmapExtension { 21 | lb_pair: Pubkey::default(), 22 | positive_bin_array_bitmap: [[0; 8]; EXTENSION_BIN_ARRAY_BITMAP_SIZE], 23 | negative_bin_array_bitmap: [[0; 8]; EXTENSION_BIN_ARRAY_BITMAP_SIZE], 24 | } 25 | } 26 | } 27 | 28 | impl BinArrayBitmapExtension { 29 | pub fn initialize(&mut self, lb_pair: Pubkey) { 30 | self.lb_pair = lb_pair; 31 | self.positive_bin_array_bitmap = [[0; 8]; EXTENSION_BIN_ARRAY_BITMAP_SIZE]; 32 | self.negative_bin_array_bitmap = [[0; 8]; EXTENSION_BIN_ARRAY_BITMAP_SIZE]; 33 | } 34 | 35 | fn get_bitmap_offset(bin_array_index: i32) -> Result { 36 | // bin_array_index starts from 512 in positive side and -513 in negative side 37 | let offset = if bin_array_index > 0 { 38 | bin_array_index / BIN_ARRAY_BITMAP_SIZE - 1 39 | } else { 40 | -(bin_array_index + 1) / BIN_ARRAY_BITMAP_SIZE - 1 41 | }; 42 | Ok(offset as usize) 43 | } 44 | 45 | /// According to the given bin array index, calculate its corresponding binarray and then find the bitmap it belongs to. 46 | fn get_bitmap(&self, bin_array_index: i32) -> Result<(usize, [u64; 8]), &'static str> { 47 | let offset = Self::get_bitmap_offset(bin_array_index)?; 48 | if bin_array_index < 0 { 49 | Ok((offset, self.negative_bin_array_bitmap[offset])) 50 | } else { 51 | Ok((offset, self.positive_bin_array_bitmap[offset])) 52 | } 53 | } 54 | 55 | fn bin_array_offset_in_bitmap(bin_array_index: i32) -> Result { 56 | if bin_array_index > 0 { 57 | Ok(bin_array_index.safe_rem(BIN_ARRAY_BITMAP_SIZE)? as usize) 58 | } else { 59 | Ok((-(bin_array_index + 1)).safe_rem(BIN_ARRAY_BITMAP_SIZE)? as usize) 60 | } 61 | } 62 | 63 | fn to_bin_array_index( 64 | offset: usize, 65 | bin_array_offset: usize, 66 | is_positive: bool, 67 | ) -> Result { 68 | let offset = offset as i32; 69 | let bin_array_offset = bin_array_offset as i32; 70 | if is_positive { 71 | Ok((offset + 1) * BIN_ARRAY_BITMAP_SIZE + bin_array_offset) 72 | } else { 73 | Ok(-((offset + 1) * BIN_ARRAY_BITMAP_SIZE + bin_array_offset) - 1) 74 | } 75 | } 76 | 77 | /// Flip the value of bin in the bitmap. 78 | pub fn flip_bin_array_bit(&mut self, bin_array_index: i32) -> Result<(), &'static str> { 79 | // TODO do we need validate bin_array_index again? 80 | let (offset, bin_array_bitmap) = self.get_bitmap(bin_array_index).unwrap(); 81 | let bin_array_offset_in_bitmap = Self::bin_array_offset_in_bitmap(bin_array_index).unwrap(); 82 | let bin_array_bitmap = U512::from_limbs(bin_array_bitmap); 83 | 84 | let mask = one::<512, 8>() << bin_array_offset_in_bitmap; 85 | if bin_array_index < 0 { 86 | self.negative_bin_array_bitmap[offset as usize] = 87 | bin_array_bitmap.bitxor(mask).into_limbs(); 88 | } else { 89 | self.positive_bin_array_bitmap[offset as usize] = 90 | bin_array_bitmap.bitxor(mask).into_limbs(); 91 | } 92 | Ok(()) 93 | } 94 | 95 | pub fn bit(&self, bin_array_index: i32) -> Result { 96 | let (_, bin_array_bitmap) = self.get_bitmap(bin_array_index)?; 97 | let bin_array_offset_in_bitmap = Self::bin_array_offset_in_bitmap(bin_array_index)?; 98 | let bin_array_bitmap = U512::from_limbs(bin_array_bitmap); 99 | return Ok(bin_array_bitmap.bit(bin_array_offset_in_bitmap as usize)); 100 | } 101 | pub fn bitmap_range() -> (i32, i32) { 102 | return ( 103 | -BIN_ARRAY_BITMAP_SIZE * (EXTENSION_BIN_ARRAY_BITMAP_SIZE as i32 + 1), 104 | BIN_ARRAY_BITMAP_SIZE * (EXTENSION_BIN_ARRAY_BITMAP_SIZE as i32 + 1) - 1, 105 | ); 106 | } 107 | 108 | pub fn iter_bitmap(&self, start_index: i32, end_index: i32) -> Result, &'static str> { 109 | let offset: usize = Self::get_bitmap_offset(start_index)?; 110 | let bin_array_offset = Self::bin_array_offset_in_bitmap(start_index)?; 111 | if start_index < 0 { 112 | // iter in negative_bin_array_bitmap 113 | if start_index <= end_index { 114 | for i in (0..=offset).rev() { 115 | let mut bin_array_bitmap = U512::from_limbs(self.negative_bin_array_bitmap[i]); 116 | 117 | if i == offset { 118 | bin_array_bitmap = bin_array_bitmap 119 | << BIN_ARRAY_BITMAP_SIZE as usize - bin_array_offset - 1; 120 | if bin_array_bitmap.eq(&U512::ZERO) { 121 | continue; 122 | } 123 | 124 | let bin_array_offset_in_bitmap = 125 | bin_array_offset - bin_array_bitmap.leading_zeros(); 126 | 127 | return Ok(Some(BinArrayBitmapExtension::to_bin_array_index( 128 | i, 129 | bin_array_offset_in_bitmap, 130 | false, 131 | )?)); 132 | } 133 | if bin_array_bitmap.eq(&U512::ZERO) { 134 | continue; 135 | } 136 | let bin_array_offset_in_bitmap = 137 | BIN_ARRAY_BITMAP_SIZE as usize - bin_array_bitmap.leading_zeros() - 1; 138 | return Ok(Some(BinArrayBitmapExtension::to_bin_array_index( 139 | i, 140 | bin_array_offset_in_bitmap, 141 | false, 142 | )?)); 143 | } 144 | } else { 145 | for i in offset..EXTENSION_BIN_ARRAY_BITMAP_SIZE { 146 | let mut bin_array_bitmap = U512::from_limbs(self.negative_bin_array_bitmap[i]); 147 | if i == offset { 148 | bin_array_bitmap = bin_array_bitmap >> bin_array_offset; 149 | if bin_array_bitmap.eq(&U512::ZERO) { 150 | continue; 151 | } 152 | 153 | let bin_array_offset_in_bitmap = 154 | bin_array_offset + bin_array_bitmap.trailing_zeros(); 155 | 156 | return Ok(Some(BinArrayBitmapExtension::to_bin_array_index( 157 | i, 158 | bin_array_offset_in_bitmap, 159 | false, 160 | )?)); 161 | } 162 | 163 | if bin_array_bitmap.eq(&U512::ZERO) { 164 | continue; 165 | } 166 | let bin_array_offset_in_bitmap = bin_array_bitmap.trailing_zeros(); 167 | 168 | return Ok(Some(BinArrayBitmapExtension::to_bin_array_index( 169 | i, 170 | bin_array_offset_in_bitmap, 171 | false, 172 | )?)); 173 | } 174 | } 175 | } else { 176 | // iter in positive_bin_array_bitmap 177 | if start_index <= end_index { 178 | for i in offset..EXTENSION_BIN_ARRAY_BITMAP_SIZE { 179 | let mut bin_array_bitmap = U512::from_limbs(self.positive_bin_array_bitmap[i]); 180 | if i == offset { 181 | bin_array_bitmap = bin_array_bitmap >> bin_array_offset; 182 | if bin_array_bitmap.eq(&U512::ZERO) { 183 | continue; 184 | } 185 | 186 | let bin_array_offset_in_bitmap = 187 | bin_array_offset + bin_array_bitmap.trailing_zeros(); 188 | return Ok(Some(BinArrayBitmapExtension::to_bin_array_index( 189 | i, 190 | bin_array_offset_in_bitmap, 191 | true, 192 | )?)); 193 | } 194 | 195 | if bin_array_bitmap.eq(&U512::ZERO) { 196 | continue; 197 | } 198 | 199 | let bin_array_offset_in_bitmap = bin_array_bitmap.trailing_zeros(); 200 | return Ok(Some(BinArrayBitmapExtension::to_bin_array_index( 201 | i, 202 | bin_array_offset_in_bitmap, 203 | true, 204 | )?)); 205 | } 206 | } else { 207 | for i in (0..=offset).rev() { 208 | let mut bin_array_bitmap = U512::from_limbs(self.positive_bin_array_bitmap[i]); 209 | 210 | if i == offset { 211 | bin_array_bitmap = bin_array_bitmap 212 | << BIN_ARRAY_BITMAP_SIZE as usize - bin_array_offset - 1; 213 | 214 | if bin_array_bitmap.eq(&U512::ZERO) { 215 | continue; 216 | } 217 | let bin_array_offset_in_bitmap = 218 | bin_array_offset - bin_array_bitmap.leading_zeros(); 219 | return Ok(Some(BinArrayBitmapExtension::to_bin_array_index( 220 | i, 221 | bin_array_offset_in_bitmap, 222 | true, 223 | )?)); 224 | } 225 | 226 | if bin_array_bitmap.eq(&U512::ZERO) { 227 | continue; 228 | } 229 | let bin_array_offset_in_bitmap = 230 | BIN_ARRAY_BITMAP_SIZE as usize - bin_array_bitmap.leading_zeros() - 1; 231 | return Ok(Some(BinArrayBitmapExtension::to_bin_array_index( 232 | i, 233 | bin_array_offset_in_bitmap, 234 | true, 235 | )?)); 236 | } 237 | } 238 | } 239 | Ok(None) 240 | } 241 | 242 | pub fn next_bin_array_index_with_liquidity( 243 | &self, 244 | swap_for_y: bool, 245 | start_index: i32, 246 | ) -> Result<(i32, bool), &'static str> { 247 | let (min_bitmap_id, max_bit_map_id) = BinArrayBitmapExtension::bitmap_range(); 248 | if start_index > 0 { 249 | if swap_for_y { 250 | match self.iter_bitmap(start_index, BIN_ARRAY_BITMAP_SIZE)? { 251 | Some(value) => return Ok((value, true)), 252 | None => return Ok((BIN_ARRAY_BITMAP_SIZE - 1, false)), 253 | } 254 | } else { 255 | match self.iter_bitmap(start_index, max_bit_map_id)? { 256 | Some(value) => return Ok((value, true)), 257 | None => return Err("LBError::CannotFindNonZeroLiquidityBinArrayId"), 258 | } 259 | } 260 | } else { 261 | if swap_for_y { 262 | match self.iter_bitmap(start_index, min_bitmap_id)? { 263 | Some(value) => return Ok((value, true)), 264 | None => return Err("LBError::CannotFindNonZeroLiquidityBinArrayId"), 265 | } 266 | } else { 267 | match self.iter_bitmap(start_index, -BIN_ARRAY_BITMAP_SIZE - 1)? { 268 | Some(value) => return Ok((value, true)), 269 | None => return Ok((-BIN_ARRAY_BITMAP_SIZE, false)), 270 | } 271 | } 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/formula/dlmm/constant.rs: -------------------------------------------------------------------------------- 1 | pub const NUM_REWARDS: usize = 2; 2 | pub const SCALE_OFFSET: u8 = 64; 3 | pub const BASIS_POINT_MAX: i32 = 10000; 4 | pub const MAX_BIN_PER_ARRAY: usize = 70; 5 | pub const FEE_PRECISION: u64 = 1_000_000_000; 6 | pub const MAX_FEE_RATE: u64 = 100_000_000; 7 | pub const EXTENSION_BIN_ARRAY_BITMAP_SIZE: usize = 12; 8 | pub const BIN_ARRAY_BITMAP_SIZE: i32 = 512; 9 | pub const MIN_BIN_ID: i32 = -443636; 10 | pub const MAX_BIN_ID: i32 = 443636; 11 | 12 | pub const BIN_ARRAY: &[u8] = b"bin_array"; -------------------------------------------------------------------------------- /src/formula/dlmm/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constant; 2 | pub mod bin; 3 | pub mod u128x128_math; 4 | pub mod utils_math; 5 | pub mod u64x64_math; 6 | pub mod safe_math; 7 | pub mod bin_array_bitmap_extension; 8 | mod test; -------------------------------------------------------------------------------- /src/formula/dlmm/safe_math.rs: -------------------------------------------------------------------------------- 1 | use std::panic::Location; 2 | use ruint::aliases::U256; 3 | use solana_sdk::msg; 4 | 5 | pub trait SafeMath: Sized { 6 | fn safe_add(self, rhs: Self) -> Result; 7 | fn safe_mul(self, rhs: Self) -> Result; 8 | fn safe_div(self, rhs: Self) -> Result; 9 | fn safe_rem(self, rhs: Self) -> Result; 10 | fn safe_sub(self, rhs: Self) -> Result; 11 | fn safe_shl(self, offset: T) -> Result; 12 | fn safe_shr(self, offset: T) -> Result; 13 | } 14 | 15 | macro_rules! checked_impl { 16 | ($t:ty, $offset:ty) => { 17 | impl SafeMath<$offset> for $t { 18 | #[inline(always)] 19 | fn safe_add(self, v: $t) -> Result<$t, &'static str> { 20 | match self.checked_add(v) { 21 | Some(result) => Ok(result), 22 | None => { 23 | let caller = Location::caller(); 24 | msg!("Math error thrown at {}:{}", caller.file(), caller.line()); 25 | Err("LBError::MathOverflow") 26 | } 27 | } 28 | } 29 | 30 | #[inline(always)] 31 | fn safe_sub(self, v: $t) -> Result<$t, &'static str> { 32 | match self.checked_sub(v) { 33 | Some(result) => Ok(result), 34 | None => { 35 | let caller = Location::caller(); 36 | msg!("Math error thrown at {}:{}", caller.file(), caller.line()); 37 | Err("LBError::MathOverflow") 38 | } 39 | } 40 | } 41 | 42 | #[inline(always)] 43 | fn safe_mul(self, v: $t) -> Result<$t, &'static str> { 44 | match self.checked_mul(v) { 45 | Some(result) => Ok(result), 46 | None => { 47 | let caller = Location::caller(); 48 | msg!("Math error thrown at {}:{}", caller.file(), caller.line()); 49 | Err("LBError::MathOverflow") 50 | } 51 | } 52 | } 53 | 54 | #[inline(always)] 55 | fn safe_div(self, v: $t) -> Result<$t, &'static str> { 56 | match self.checked_div(v) { 57 | Some(result) => Ok(result), 58 | None => { 59 | let caller = Location::caller(); 60 | msg!("Math error thrown at {}:{}", caller.file(), caller.line()); 61 | Err("LBError::MathOverflow") 62 | } 63 | } 64 | } 65 | 66 | #[inline(always)] 67 | fn safe_rem(self, v: $t) -> Result<$t, &'static str> { 68 | match self.checked_rem(v) { 69 | Some(result) => Ok(result), 70 | None => { 71 | let caller = Location::caller(); 72 | msg!("Math error thrown at {}:{}", caller.file(), caller.line()); 73 | Err("LBError::MathOverflow") 74 | } 75 | } 76 | } 77 | 78 | #[inline(always)] 79 | fn safe_shl(self, v: $offset) -> Result<$t, &'static str> { 80 | match self.checked_shl(v) { 81 | Some(result) => Ok(result), 82 | None => { 83 | let caller = Location::caller(); 84 | msg!("Math error thrown at {}:{}", caller.file(), caller.line()); 85 | Err("LBError::MathOverflow") 86 | } 87 | } 88 | } 89 | 90 | #[inline(always)] 91 | fn safe_shr(self, v: $offset) -> Result<$t, &'static str> { 92 | match self.checked_shr(v) { 93 | Some(result) => Ok(result), 94 | None => { 95 | let caller = Location::caller(); 96 | msg!("Math error thrown at {}:{}", caller.file(), caller.line()); 97 | Err("LBError::MathOverflow") 98 | } 99 | } 100 | } 101 | } 102 | }; 103 | } 104 | 105 | checked_impl!(u16, u32); 106 | checked_impl!(i32, u32); 107 | checked_impl!(u32, u32); 108 | checked_impl!(u64, u32); 109 | checked_impl!(i64, u32); 110 | checked_impl!(u128, u32); 111 | checked_impl!(i128, u32); 112 | checked_impl!(usize, u32); 113 | checked_impl!(U256, usize); 114 | -------------------------------------------------------------------------------- /src/formula/dlmm/test/meteora_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test { 3 | use std::collections::HashMap; 4 | use std::rc::Rc; 5 | use std::str::FromStr; 6 | use solana_client::nonblocking::rpc_client::RpcClient; 7 | use solana_sdk::clock::Clock; 8 | use solana_sdk::pubkey::Pubkey; 9 | use crate::formula::dlmm::bin::BinArray; 10 | use crate::formula::meteora_dlmm::{get_bin_array_pubkeys_for_swap, quote_exact_in, quote_exact_out}; 11 | use crate::r#struct::account::AccountDataSerializer; 12 | use crate::r#struct::pools::MeteoraDlmmMarket; 13 | 14 | async fn get_clock(rpc_client: RpcClient) -> Result { 15 | let clock_account = rpc_client 16 | .get_account(&Pubkey::from_str("SysvarC1ock11111111111111111111111111111111").unwrap()) 17 | .await 18 | .unwrap(); 19 | 20 | let clock_state: Clock = bincode::deserialize(clock_account.data.as_ref()).unwrap(); 21 | 22 | Ok(clock_state) 23 | } 24 | 25 | #[tokio::test] 26 | async fn test_swap_quote_exact_out() { 27 | // RPC client. No gPA is required. 28 | let alchemy = "https://solana-mainnet.g.alchemy.com/v2/76-rZCjoPGCHXLfjHNojk5CiqX8I36AT".to_string(); 29 | let rpc_client = RpcClient::new(alchemy); 30 | 31 | let SOL_USDC = Pubkey::from_str("HTvjzsfX3yU6BUodCjZ5vZkUrAxMDTrBs3CJaq43ashR").unwrap(); 32 | 33 | // let lb_pair = program.account::(SOL_USDC).await.unwrap(); 34 | 35 | let data = rpc_client.get_account_data(&SOL_USDC).await.unwrap(); 36 | let lb_pair = MeteoraDlmmMarket::unpack_data(&data); 37 | 38 | // 3 bin arrays to left, and right is enough to cover most of the swap, and stay under 1.4m CU constraint. 39 | // Get 3 bin arrays to the left from the active bin 40 | let left_bin_array_pubkeys = 41 | get_bin_array_pubkeys_for_swap(SOL_USDC, &lb_pair, None, true, 3).unwrap(); 42 | 43 | // Get 3 bin arrays to the right the from active bin 44 | let right_bin_array_pubkeys = 45 | get_bin_array_pubkeys_for_swap(SOL_USDC, &lb_pair, None, false, 3).unwrap(); 46 | 47 | // Fetch bin arrays 48 | let bin_array_pubkeys = left_bin_array_pubkeys 49 | .into_iter() 50 | .chain(right_bin_array_pubkeys.into_iter()) 51 | .collect::>(); 52 | 53 | let accounts = rpc_client 54 | .get_multiple_accounts(&bin_array_pubkeys) 55 | .await 56 | .unwrap(); 57 | 58 | let bin_arrays = accounts 59 | .into_iter() 60 | .zip(bin_array_pubkeys.into_iter()) 61 | .map(|(account, key)| { 62 | ( 63 | key, 64 | BinArray::unpack_data(&account.unwrap().data) 65 | ) 66 | }) 67 | .collect::>(); 68 | 69 | let usdc_token_multiplier = 1_000_000.0; 70 | let sol_token_multiplier = 1_000_000_000.0; 71 | 72 | let out_sol_amount = 1_000_000_000; 73 | let clock = get_clock(rpc_client).await.unwrap(); 74 | 75 | let quote_result = quote_exact_out( 76 | SOL_USDC, 77 | &lb_pair, 78 | out_sol_amount, 79 | false, 80 | bin_arrays.clone(), 81 | None, 82 | clock.unix_timestamp as u64, 83 | clock.slot, 84 | ) 85 | .unwrap(); 86 | 87 | let in_amount = quote_result.amount_in + quote_result.fee; 88 | 89 | println!( 90 | "{} USDC -> exact 1 SOL", 91 | in_amount as f64 / usdc_token_multiplier 92 | ); 93 | 94 | let quote_result = quote_exact_in( 95 | SOL_USDC, 96 | &lb_pair, 97 | in_amount, 98 | false, 99 | bin_arrays.clone(), 100 | None, 101 | clock.unix_timestamp as u64, 102 | clock.slot, 103 | ) 104 | .unwrap(); 105 | 106 | println!( 107 | "{} USDC -> {} SOL", 108 | in_amount as f64 / usdc_token_multiplier, 109 | quote_result.amount_out as f64 / sol_token_multiplier 110 | ); 111 | 112 | let out_usdc_amount = 200_000_000; 113 | 114 | let quote_result = quote_exact_out( 115 | SOL_USDC, 116 | &lb_pair, 117 | out_usdc_amount, 118 | true, 119 | bin_arrays.clone(), 120 | None, 121 | clock.unix_timestamp as u64, 122 | clock.slot, 123 | ) 124 | .unwrap(); 125 | 126 | let in_amount = quote_result.amount_in + quote_result.fee; 127 | 128 | println!( 129 | "{} SOL -> exact 200 USDC", 130 | in_amount as f64 / sol_token_multiplier 131 | ); 132 | 133 | let quote_result = quote_exact_in( 134 | SOL_USDC, 135 | &lb_pair, 136 | in_amount, 137 | true, 138 | bin_arrays, 139 | None, 140 | clock.unix_timestamp as u64, 141 | clock.slot, 142 | ) 143 | .unwrap(); 144 | 145 | println!( 146 | "{} SOL -> {} USDC", 147 | in_amount as f64 / sol_token_multiplier, 148 | quote_result.amount_out as f64 / usdc_token_multiplier 149 | ); 150 | } 151 | 152 | #[tokio::test] 153 | async fn test_swap_quote_exact_in() { 154 | // RPC client. No gPA is required. 155 | let alchemy = "https://solana-mainnet.g.alchemy.com/v2/76-rZCjoPGCHXLfjHNojk5CiqX8I36AT".to_string(); 156 | let rpc_client = RpcClient::new(alchemy); 157 | 158 | let SOL_USDC = Pubkey::from_str("HTvjzsfX3yU6BUodCjZ5vZkUrAxMDTrBs3CJaq43ashR").unwrap(); 159 | 160 | let data = rpc_client.get_account_data(&SOL_USDC).await.unwrap(); 161 | let lb_pair = MeteoraDlmmMarket::unpack_data(&data); 162 | 163 | // 3 bin arrays to left, and right is enough to cover most of the swap, and stay under 1.4m CU constraint. 164 | // Get 3 bin arrays to the left from the active bin 165 | let left_bin_array_pubkeys = 166 | get_bin_array_pubkeys_for_swap(SOL_USDC, &lb_pair, None, true, 3).unwrap(); 167 | 168 | // Get 3 bin arrays to the right the from active bin 169 | let right_bin_array_pubkeys = 170 | get_bin_array_pubkeys_for_swap(SOL_USDC, &lb_pair, None, false, 3).unwrap(); 171 | 172 | // Fetch bin arrays 173 | let bin_array_pubkeys = left_bin_array_pubkeys 174 | .into_iter() 175 | .chain(right_bin_array_pubkeys.into_iter()) 176 | .collect::>(); 177 | 178 | let accounts = rpc_client 179 | .get_multiple_accounts(&bin_array_pubkeys) 180 | .await 181 | .unwrap(); 182 | 183 | let bin_arrays = accounts 184 | .into_iter() 185 | .zip(bin_array_pubkeys.into_iter()) 186 | .map(|(account, key)| { 187 | ( 188 | key, 189 | BinArray::unpack_data(&account.unwrap().data) 190 | ) 191 | }) 192 | .collect::>(); 193 | 194 | // 1 SOL -> USDC 195 | let in_sol_amount = 1_000_000_000; 196 | 197 | let clock = get_clock(rpc_client).await.unwrap(); 198 | 199 | let quote_result = quote_exact_in( 200 | SOL_USDC, 201 | &lb_pair, 202 | in_sol_amount, 203 | true, 204 | bin_arrays.clone(), 205 | None, 206 | clock.unix_timestamp as u64, 207 | clock.slot, 208 | ) 209 | .unwrap(); 210 | 211 | println!( 212 | "1 SOL -> {:?} USDC", 213 | quote_result.amount_out as f64 / 1_000_000.0 214 | ); 215 | 216 | // 100 USDC -> SOL 217 | let in_usdc_amount = 100_000_000; 218 | 219 | let quote_result = quote_exact_in( 220 | SOL_USDC, 221 | &lb_pair, 222 | in_usdc_amount, 223 | false, 224 | bin_arrays.clone(), 225 | None, 226 | clock.unix_timestamp as u64, 227 | clock.slot, 228 | ) 229 | .unwrap(); 230 | 231 | println!( 232 | "100 USDC -> {:?} SOL", 233 | quote_result.amount_out as f64 / 1_000_000_000.0 234 | ); 235 | } 236 | } -------------------------------------------------------------------------------- /src/formula/dlmm/test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod meteora_test; -------------------------------------------------------------------------------- /src/formula/dlmm/u128x128_math.rs: -------------------------------------------------------------------------------- 1 | use ruint::aliases::U256; 2 | 3 | // Round up, down 4 | #[derive(PartialEq)] 5 | pub enum Rounding { 6 | Up, 7 | Down, 8 | } 9 | 10 | /// (x * y) / denominator 11 | pub fn mul_div(x: u128, y: u128, denominator: u128, rounding: Rounding) -> Option { 12 | if denominator == 0 { 13 | return None; 14 | } 15 | 16 | let x = U256::from(x); 17 | let y = U256::from(y); 18 | let denominator = U256::from(denominator); 19 | 20 | let prod = x.checked_mul(y)?; 21 | 22 | match rounding { 23 | Rounding::Up => prod.div_ceil(denominator).try_into().ok(), 24 | Rounding::Down => { 25 | let (quotient, _) = prod.div_rem(denominator); 26 | quotient.try_into().ok() 27 | } 28 | } 29 | } 30 | 31 | /// (x * y) >> offset 32 | #[inline] 33 | pub fn mul_shr(x: u128, y: u128, offset: u8, rounding: Rounding) -> Option { 34 | let denominator = 1u128.checked_shl(offset.into())?; 35 | mul_div(x, y, denominator, rounding) 36 | } 37 | 38 | /// (x << offset) / y 39 | #[inline] 40 | pub fn shl_div(x: u128, y: u128, offset: u8, rounding: Rounding) -> Option { 41 | let scale = 1u128.checked_shl(offset.into())?; 42 | mul_div(x, scale, y, rounding) 43 | } 44 | -------------------------------------------------------------------------------- /src/formula/dlmm/u64x64_math.rs: -------------------------------------------------------------------------------- 1 | use ruint::aliases::U256; 2 | use crate::formula::dlmm::constant::BASIS_POINT_MAX; 3 | 4 | // Precision when converting from decimal to fixed point. Or the other way around. 10^12 5 | pub const PRECISION: u128 = 1_000_000_000_000; 6 | 7 | // Number of bits to scale. This will decide the position of the radix point. 8 | pub const SCALE_OFFSET: u8 = 64; 9 | 10 | // Where does this value come from ? 11 | // When smallest bin is used (1 bps), the maximum of bin limit is 887272 (Check: https://docs.traderjoexyz.com/concepts/bin-math). 12 | // But in solana, the token amount is represented in 64 bits, therefore, it will be (1 + 0.0001)^n < 2 ** 64, solve for n, n ~= 443636 13 | // Then we calculate bits needed to represent 443636 exponential, 2^n >= 443636, ~= 19 14 | // If we convert 443636 to binary form, it will be 1101100010011110100 (19 bits). 15 | // Which, the 19 bits are the bits the binary exponential will loop through. 16 | // The 20th bit will be 0x80000, which the exponential already > the maximum number of bin Q64.64 can support 17 | const MAX_EXPONENTIAL: u32 = 0x80000; // 1048576 18 | 19 | // 1.0000... representation of 64x64 20 | pub const ONE: u128 = 1u128 << SCALE_OFFSET; 21 | 22 | pub fn pow(base: u128, exp: i32) -> Option { 23 | // If exponent is negative. We will invert the result later by 1 / base^exp.abs() 24 | let mut invert = exp.is_negative(); 25 | 26 | // When exponential is 0, result will always be 1 27 | if exp == 0 { 28 | return Some(1u128 << 64); 29 | } 30 | 31 | // Make the exponential positive. Which will compute the result later by 1 / base^exp 32 | let exp: u32 = if invert { exp.abs() as u32 } else { exp as u32 }; 33 | 34 | // No point to continue the calculation as it will overflow the maximum value Q64.64 can support 35 | if exp >= MAX_EXPONENTIAL { 36 | return None; 37 | } 38 | 39 | let mut squared_base = base; 40 | let mut result = ONE; 41 | 42 | // When multiply the base twice, the number of bits double from 128 -> 256, which overflow. 43 | // The trick here is to inverse the calculation, which make the upper 64 bits (number bits) to be 0s. 44 | // For example: 45 | // let base = 1.001, exp = 5 46 | // let neg = 1 / (1.001 ^ 5) 47 | // Inverse the neg: 1 / neg 48 | // By using a calculator, you will find out that 1.001^5 == 1 / (1 / 1.001^5) 49 | if squared_base >= result { 50 | // This inverse the base: 1 / base 51 | squared_base = u128::MAX.checked_div(squared_base)?; 52 | // If exponent is negative, the above already inverted the result. Therefore, at the end of the function, we do not need to invert again. 53 | invert = !invert; 54 | } 55 | 56 | // The following code is equivalent to looping through each binary value of the exponential. 57 | // As explained in MAX_EXPONENTIAL, 19 exponential bits are enough to covert the full bin price. 58 | // Therefore, there will be 19 if statements, which similar to the following pseudo code. 59 | /* 60 | let mut result = 1; 61 | while exponential > 0 { 62 | if exponential & 1 > 0 { 63 | result *= base; 64 | } 65 | base *= base; 66 | exponential >>= 1; 67 | } 68 | */ 69 | 70 | // From right to left 71 | // squared_base = 1 * base^1 72 | // 1st bit is 1 73 | if exp & 0x1 > 0 { 74 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 75 | } 76 | 77 | // squared_base = base^2 78 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 79 | // 2nd bit is 1 80 | if exp & 0x2 > 0 { 81 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 82 | } 83 | 84 | // Example: 85 | // If the base is 1.001, exponential is 3. Binary form of 3 is ..0011. The last 2 1's bit fulfill the above 2 bitwise condition. 86 | // The result will be 1 * base^1 * base^2 == base^3. The process continues until reach the 20th bit 87 | 88 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 89 | if exp & 0x4 > 0 { 90 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 91 | } 92 | 93 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 94 | if exp & 0x8 > 0 { 95 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 96 | } 97 | 98 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 99 | if exp & 0x10 > 0 { 100 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 101 | } 102 | 103 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 104 | if exp & 0x20 > 0 { 105 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 106 | } 107 | 108 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 109 | if exp & 0x40 > 0 { 110 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 111 | } 112 | 113 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 114 | if exp & 0x80 > 0 { 115 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 116 | } 117 | 118 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 119 | if exp & 0x100 > 0 { 120 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 121 | } 122 | 123 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 124 | if exp & 0x200 > 0 { 125 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 126 | } 127 | 128 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 129 | if exp & 0x400 > 0 { 130 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 131 | } 132 | 133 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 134 | if exp & 0x800 > 0 { 135 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 136 | } 137 | 138 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 139 | if exp & 0x1000 > 0 { 140 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 141 | } 142 | 143 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 144 | if exp & 0x2000 > 0 { 145 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 146 | } 147 | 148 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 149 | if exp & 0x4000 > 0 { 150 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 151 | } 152 | 153 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 154 | if exp & 0x8000 > 0 { 155 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 156 | } 157 | 158 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 159 | if exp & 0x10000 > 0 { 160 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 161 | } 162 | 163 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 164 | if exp & 0x20000 > 0 { 165 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 166 | } 167 | 168 | squared_base = (squared_base.checked_mul(squared_base)?) >> SCALE_OFFSET; 169 | if exp & 0x40000 > 0 { 170 | result = (result.checked_mul(squared_base)?) >> SCALE_OFFSET 171 | } 172 | 173 | // Stop here as the next is 20th bit, which > MAX_EXPONENTIAL 174 | if result == 0 { 175 | return None; 176 | } 177 | 178 | if invert { 179 | result = u128::MAX.checked_div(result)?; 180 | } 181 | 182 | Some(result) 183 | } 184 | 185 | // Helper function to convert fixed point number to decimal with 10^12 precision. Decimal form is not being used in program, it's only for UI purpose. 186 | pub fn to_decimal(value: u128) -> Option { 187 | let value = U256::from(value); 188 | let precision = U256::from(PRECISION); 189 | let scaled_value = value.checked_mul(precision)?; 190 | // ruint checked math is different with the rust std u128. If there's bit with 1 value being shifted out, it will return None. Therefore, we use overflowing_shr 191 | let (scaled_down_value, _) = scaled_value.overflowing_shr(SCALE_OFFSET.into()); 192 | scaled_down_value.try_into().ok() 193 | } 194 | 195 | // Helper function to convert decimal with 10^12 precision to fixed point number 196 | pub fn from_decimal(value: u128) -> Option { 197 | let value = U256::from(value); 198 | let precision = U256::from(PRECISION); 199 | let (q_value, _) = value.overflowing_shl(SCALE_OFFSET.into()); 200 | let fp_value = q_value.checked_div(precision)?; 201 | fp_value.try_into().ok() 202 | } 203 | 204 | // Helper function to get the base for price calculation. Eg: 1.001 in 64x64 representation 205 | pub fn get_base(bin_step: u32) -> Option { 206 | let quotient = u128::from(bin_step).checked_shl(SCALE_OFFSET.into())?; 207 | let fraction = quotient.checked_div(BASIS_POINT_MAX as u128)?; 208 | ONE.checked_add(fraction) 209 | } 210 | -------------------------------------------------------------------------------- /src/formula/dlmm/utils_math.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | safe_math::SafeMath, 3 | u128x128_math::{mul_div, mul_shr, shl_div, Rounding}, 4 | u64x64_math::pow, 5 | }; 6 | use num_traits::cast::FromPrimitive; 7 | use ruint::aliases::U256; 8 | use ruint::Uint; 9 | 10 | #[inline] 11 | pub fn safe_pow_cast(base: u128, exp: i32) -> Result { 12 | T::from_u128(pow(base, exp).ok_or_else(|| "LBError::MathOverflow")?) 13 | .ok_or_else(|| "LBError::TypeCastFailed") 14 | } 15 | 16 | #[inline] 17 | pub fn safe_mul_div_cast( 18 | x: u128, 19 | y: u128, 20 | denominator: u128, 21 | rounding: Rounding, 22 | ) -> Result { 23 | T::from_u128(mul_div(x, y, denominator, rounding).ok_or_else(|| "LBError::MathOverflow")?) 24 | .ok_or_else(|| "LBError::TypeCastFailed") 25 | } 26 | 27 | #[inline] 28 | pub fn safe_mul_div_cast_from_u64_to_u64(x: u64, y: u64, denominator: u64) -> Result { 29 | let x = u128::from(x); 30 | let y = u128::from(y); 31 | let denominator = u128::from(denominator); 32 | let result = u64::try_from(x.safe_mul(y)?.safe_div(denominator)?) 33 | .map_err(|_| "LBError::TypeCastFailed")?; 34 | Ok(result) 35 | } 36 | 37 | #[inline] 38 | pub fn safe_mul_div_cast_from_u256_to_u64(x: u64, y: U256, denominator: U256) -> Result { 39 | let x = U256::from(x); 40 | // let denominator = U256::from(denominator); 41 | let result = u64::try_from(x.safe_mul(y)?.safe_div(denominator)?) 42 | .map_err(|_| "LBError::TypeCastFailed")?; 43 | Ok(result) 44 | } 45 | 46 | #[inline] 47 | pub fn safe_mul_shr_cast( 48 | x: u128, 49 | y: u128, 50 | offset: u8, 51 | rounding: Rounding, 52 | ) -> Result { 53 | T::from_u128(mul_shr(x, y, offset, rounding).ok_or_else(|| "LBError::MathOverflow")?) 54 | .ok_or_else(|| "LBError::TypeCastFailed") 55 | } 56 | 57 | #[inline] 58 | pub fn safe_shl_div_cast( 59 | x: u128, 60 | y: u128, 61 | offset: u8, 62 | rounding: Rounding, 63 | ) -> Result { 64 | T::from_u128(shl_div(x, y, offset, rounding).ok_or_else(|| "LBError::MathOverflow")?) 65 | .ok_or_else(|| "LBError::TypeCastFailed") 66 | } 67 | 68 | pub const fn one() -> Uint { 69 | let mut words = [0; LIMBS]; 70 | words[0] = 1; 71 | Uint::from_limbs(words) 72 | } 73 | -------------------------------------------------------------------------------- /src/formula/meteora_dlmm.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::str::FromStr; 3 | use anyhow::Context; 4 | use num_enum::{IntoPrimitive, TryFromPrimitive}; 5 | use solana_sdk::pubkey::Pubkey; 6 | use crate::constants::METEORA_DLMM_PROGRAM_PUBKEY; 7 | use crate::formula::dlmm::bin::{Bin, BinArray, SwapResult}; 8 | use crate::formula::dlmm::bin_array_bitmap_extension::BinArrayBitmapExtension; 9 | use crate::formula::dlmm::constant::BIN_ARRAY; 10 | use crate::r#struct::pools::MeteoraDlmmMarket; 11 | 12 | pub fn quote_exact_out( 13 | lb_pair_pubkey: Pubkey, 14 | lb_pair: &MeteoraDlmmMarket, 15 | mut amount_out: u64, 16 | swap_for_y: bool, 17 | bin_arrays: HashMap, 18 | bitmap_extension: Option<&BinArrayBitmapExtension>, 19 | current_timestamp: u64, 20 | current_slot: u64, 21 | ) -> Result { 22 | validate_swap_activation(lb_pair, current_timestamp, current_slot)?; 23 | 24 | let mut lb_pair = *lb_pair; 25 | lb_pair.update_references(current_timestamp as i64)?; 26 | 27 | let mut total_amount_in: u64 = 0; 28 | let mut total_fee: u64 = 0; 29 | 30 | while amount_out > 0 { 31 | let active_bin_array_pubkey = get_bin_array_pubkeys_for_swap( 32 | lb_pair_pubkey, 33 | &lb_pair, 34 | bitmap_extension, 35 | swap_for_y, 36 | 1, 37 | )? 38 | .pop() 39 | .context("Pool out of liquidity").expect("Pool out of liquidity"); 40 | 41 | let mut active_bin_array = bin_arrays 42 | .get(&active_bin_array_pubkey) 43 | .cloned() 44 | .context("Active bin array not found").expect("Active bin array not found"); 45 | 46 | loop { 47 | if active_bin_array 48 | .is_bin_id_within_range(lb_pair.active_id) 49 | .is_err() 50 | || amount_out == 0 51 | { 52 | break; 53 | } 54 | 55 | lb_pair.update_volatility_accumulator()?; 56 | 57 | let active_bin = active_bin_array.get_bin_mut(lb_pair.active_id)?; 58 | let price = active_bin.get_or_store_bin_price(lb_pair.active_id, lb_pair.bin_step)?; 59 | 60 | if !active_bin.is_empty(!swap_for_y) { 61 | let bin_max_amount_out = active_bin.get_max_amount_out(swap_for_y); 62 | if amount_out >= bin_max_amount_out { 63 | let max_amount_in = active_bin.get_max_amount_in(price, swap_for_y)?; 64 | let max_fee = lb_pair.compute_fee(max_amount_in)?; 65 | 66 | total_amount_in = total_amount_in 67 | .checked_add(max_amount_in) 68 | .context("MathOverflow").expect("MathOverflow"); 69 | 70 | total_fee = total_fee.checked_add(max_fee).context("MathOverflow").expect("MathOverflow"); 71 | 72 | amount_out = amount_out 73 | .checked_sub(bin_max_amount_out) 74 | .context("MathOverflow").expect("MathOverflow"); 75 | } else { 76 | let amount_in = Bin::get_amount_in(amount_out, price, swap_for_y)?; 77 | let fee = lb_pair.compute_fee(amount_in)?; 78 | 79 | total_amount_in = total_amount_in 80 | .checked_add(amount_in) 81 | .context("MathOverflow").expect("MathOverflow"); 82 | 83 | total_fee = total_fee.checked_add(fee).context("MathOverflow").expect("MathOverflow"); 84 | 85 | amount_out = 0; 86 | } 87 | } 88 | 89 | if amount_out > 0 { 90 | lb_pair.advance_active_bin(swap_for_y)?; 91 | } 92 | } 93 | } 94 | 95 | Ok(SwapExactOutQuote { 96 | amount_in: total_amount_in, 97 | fee: total_fee, 98 | }) 99 | } 100 | 101 | pub fn quote_exact_in( 102 | lb_pair_pubkey: Pubkey, 103 | lb_pair: &MeteoraDlmmMarket, 104 | mut amount_in: u64, 105 | swap_for_y: bool, 106 | bin_arrays: HashMap, 107 | bitmap_extension: Option<&BinArrayBitmapExtension>, 108 | current_timestamp: u64, 109 | current_slot: u64, 110 | ) -> Result { 111 | validate_swap_activation(lb_pair, current_timestamp, current_slot)?; 112 | 113 | let mut lb_pair = *lb_pair; 114 | lb_pair.update_references(current_timestamp as i64)?; 115 | 116 | let mut total_amount_out: u64 = 0; 117 | let mut total_fee: u64 = 0; 118 | 119 | while amount_in > 0 { 120 | let active_bin_array_pubkey = get_bin_array_pubkeys_for_swap( 121 | lb_pair_pubkey, 122 | &lb_pair, 123 | bitmap_extension, 124 | swap_for_y, 125 | 1, 126 | )? 127 | .pop() 128 | .context("Pool out of liquidity").expect("Pool out of liquidity"); 129 | 130 | let mut active_bin_array = bin_arrays 131 | .get(&active_bin_array_pubkey) 132 | .cloned() 133 | .context("Active bin array not found").expect("Active bin array not found"); 134 | 135 | loop { 136 | if active_bin_array 137 | .is_bin_id_within_range(lb_pair.active_id) 138 | .is_err() 139 | || amount_in == 0 140 | { 141 | break; 142 | } 143 | 144 | lb_pair.update_volatility_accumulator()?; 145 | 146 | let active_bin = active_bin_array.get_bin_mut(lb_pair.active_id)?; 147 | let price = active_bin.get_or_store_bin_price(lb_pair.active_id, lb_pair.bin_step)?; 148 | 149 | if !active_bin.is_empty(!swap_for_y) { 150 | let SwapResult { 151 | amount_in_with_fees, 152 | amount_out, 153 | fee, 154 | .. 155 | } = active_bin.swap(amount_in, price, swap_for_y, &lb_pair, None)?; 156 | 157 | amount_in = amount_in 158 | .checked_sub(amount_in_with_fees) 159 | .context("MathOverflow").expect("MathOverflow"); 160 | 161 | total_amount_out = total_amount_out 162 | .checked_add(amount_out) 163 | .context("MathOverflow").expect("MathOverflow"); 164 | total_fee = total_fee.checked_add(fee).context("MathOverflow").expect("MathOverflow"); 165 | } 166 | 167 | if amount_in > 0 { 168 | lb_pair.advance_active_bin(swap_for_y)?; 169 | } 170 | } 171 | } 172 | 173 | Ok(SwapExactInQuote { 174 | amount_out: total_amount_out, 175 | fee: total_fee, 176 | }) 177 | } 178 | 179 | pub fn get_bin_array_pubkeys_for_swap( 180 | lb_pair_pubkey: Pubkey, 181 | lb_pair: &MeteoraDlmmMarket, 182 | bitmap_extension: Option<&BinArrayBitmapExtension>, 183 | swap_for_y: bool, 184 | take_count: u8, 185 | ) -> Result, &'static str> { 186 | let mut start_bin_array_idx = BinArray::bin_id_to_bin_array_index(lb_pair.active_id)?; 187 | let mut bin_array_idx = vec![]; 188 | let increment = if swap_for_y { -1 } else { 1 }; 189 | 190 | loop { 191 | if bin_array_idx.len() == take_count as usize { 192 | break; 193 | } 194 | 195 | if lb_pair.is_overflow_default_bin_array_bitmap(start_bin_array_idx) { 196 | let Some(bitmap_extension) = bitmap_extension else { 197 | break; 198 | }; 199 | let Ok((next_bin_array_idx, has_liquidity)) = bitmap_extension 200 | .next_bin_array_index_with_liquidity(swap_for_y, start_bin_array_idx) 201 | else { 202 | // Out of search range. No liquidity. 203 | break; 204 | }; 205 | if has_liquidity { 206 | bin_array_idx.push(next_bin_array_idx); 207 | start_bin_array_idx = next_bin_array_idx + increment; 208 | } else { 209 | // Switch to internal bitmap 210 | start_bin_array_idx = next_bin_array_idx; 211 | } 212 | } else { 213 | let Ok((next_bin_array_idx, has_liquidity)) = lb_pair 214 | .next_bin_array_index_with_liquidity_internal(swap_for_y, start_bin_array_idx) 215 | else { 216 | break; 217 | }; 218 | if has_liquidity { 219 | bin_array_idx.push(next_bin_array_idx); 220 | start_bin_array_idx = next_bin_array_idx + increment; 221 | } else { 222 | // Switch to external bitmap 223 | start_bin_array_idx = next_bin_array_idx; 224 | } 225 | } 226 | } 227 | 228 | let bin_array_pubkeys = bin_array_idx 229 | .into_iter() 230 | .map(|idx| derive_bin_array_pda(lb_pair_pubkey, idx.into()).0) 231 | .collect(); 232 | 233 | Ok(bin_array_pubkeys) 234 | } 235 | 236 | fn validate_swap_activation( 237 | lb_pair: &MeteoraDlmmMarket, 238 | current_timestamp: u64, 239 | current_slot: u64, 240 | ) -> Result<(), &'static str> { 241 | assert!(lb_pair.status()?.eq(&PairStatus::Enabled)); 242 | 243 | let pair_type = lb_pair.pair_type()?; 244 | if pair_type.eq(&PairType::Permission) { 245 | let activation_type = ActivationType::try_from(lb_pair.activation_type).expect("unknown activation_type"); 246 | let current_point = match activation_type { 247 | ActivationType::Slot => current_slot, 248 | ActivationType::Timestamp => current_timestamp, 249 | }; 250 | 251 | assert!(current_point >= lb_pair.activation_point) 252 | } 253 | 254 | Ok(()) 255 | } 256 | 257 | pub fn derive_bin_array_pda(lb_pair: Pubkey, bin_array_index: i64) -> (Pubkey, u8) { 258 | let program_id = Pubkey::from_str(METEORA_DLMM_PROGRAM_PUBKEY).unwrap(); 259 | Pubkey::find_program_address( 260 | &[BIN_ARRAY, lb_pair.as_ref(), &bin_array_index.to_le_bytes()], 261 | &program_id, 262 | ) 263 | } 264 | 265 | #[derive(Debug)] 266 | pub struct SwapExactOutQuote { 267 | pub amount_in: u64, 268 | pub fee: u64, 269 | } 270 | 271 | #[derive(Debug)] 272 | pub struct SwapExactInQuote { 273 | pub amount_out: u64, 274 | pub fee: u64, 275 | } 276 | 277 | #[derive(Copy, Clone, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] 278 | #[repr(u8)] 279 | /// Type of the Pair. 0 = Permissionless, 1 = Permission. Putting 0 as permissionless for backward compatibility. 280 | pub enum PairType { 281 | Permissionless, 282 | Permission, 283 | } 284 | 285 | #[derive(Copy, Clone, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] 286 | #[repr(u8)] 287 | /// Type of the activation 288 | pub enum ActivationType { 289 | Slot, 290 | Timestamp, 291 | } 292 | 293 | #[derive(Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] 294 | #[repr(u8)] 295 | /// Pair status. 0 = Enabled, 1 = Disabled. Putting 0 as enabled for backward compatibility. 296 | pub enum PairStatus { 297 | // Fully enabled. 298 | // Condition: 299 | // Permissionless: PairStatus::Enabled 300 | // Permission: PairStatus::Enabled and current_point > activation_point 301 | Enabled, 302 | // Similar as emergency mode. User can only withdraw (Only outflow). Except whitelisted wallet still have full privileges. 303 | Disabled, 304 | } -------------------------------------------------------------------------------- /src/formula/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod base; 2 | pub mod constant_product; 3 | pub mod clmm; 4 | pub mod raydium_clmm; 5 | pub mod orca_clmm; 6 | pub mod meteora_dlmm; 7 | pub mod dlmm; 8 | mod raydium_openbook; 9 | mod openbook; -------------------------------------------------------------------------------- /src/formula/openbook/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod openbook_processor; 2 | pub mod math; -------------------------------------------------------------------------------- /src/formula/openbook/openbook_processor.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 2 | #[repr(packed)] 3 | pub struct LeafNode { 4 | tag: u32, 5 | owner_slot: u8, 6 | fee_tier: u8, 7 | padding: [u8; 2], 8 | key: u128, 9 | owner: [u64; 4], 10 | quantity: u64, 11 | client_order_id: u64, 12 | } -------------------------------------------------------------------------------- /src/formula/orca_clmm.rs: -------------------------------------------------------------------------------- 1 | use crate::formula::clmm::concentrated_liquidity::compute_swap_step; 2 | use crate::formula::clmm::constant::{ORCA_MAX_SQRT_PRICE_X64, MIN_SQRT_PRICE_X64}; 3 | use crate::formula::clmm::orca_swap_state::{checked_mul_div, next_tick_cross_update, NO_EXPLICIT_SQRT_PRICE_LIMIT, NUM_REWARDS, PostSwapUpdate, PROTOCOL_FEE_RATE_MUL_VALUE, Q64_RESOLUTION, SwapTickSequence, Tick, TICK_ARRAY_SIZE, TickUpdate}; 4 | use crate::formula::clmm::orca_tick_math::{sqrt_price_from_tick_index, tick_index_from_sqrt_price}; 5 | use crate::formula::clmm::raydium_swap_state::add_delta; 6 | use crate::r#struct::pools::{OrcaClmmMarket, WhirlpoolRewardInfo}; 7 | 8 | pub fn swap_internal( 9 | whirlpool: &OrcaClmmMarket, 10 | swap_tick_sequence: &mut SwapTickSequence, 11 | amount: u64, 12 | sqrt_price_limit: u128, 13 | amount_specified_is_input: bool, 14 | a_to_b: bool, 15 | timestamp: u64, 16 | ) -> Result { 17 | let adjusted_sqrt_price_limit = if sqrt_price_limit == NO_EXPLICIT_SQRT_PRICE_LIMIT { 18 | if a_to_b { 19 | MIN_SQRT_PRICE_X64 20 | } else { 21 | ORCA_MAX_SQRT_PRICE_X64 22 | } 23 | } else { 24 | sqrt_price_limit 25 | }; 26 | 27 | if !(MIN_SQRT_PRICE_X64..=ORCA_MAX_SQRT_PRICE_X64).contains(&adjusted_sqrt_price_limit) { 28 | return Err("ErrorCode::SqrtPriceOutOfBounds"); 29 | } 30 | 31 | if a_to_b && adjusted_sqrt_price_limit > whirlpool.sqrt_price 32 | || !a_to_b && adjusted_sqrt_price_limit < whirlpool.sqrt_price 33 | { 34 | return Err("ErrorCode::InvalidSqrtPriceLimitDirection"); 35 | } 36 | 37 | if amount == 0 { 38 | return Err("ErrorCode::ZeroTradableAmount"); 39 | } 40 | 41 | let tick_spacing = whirlpool.tick_spacing; 42 | let fee_rate = whirlpool.fee_rate; 43 | let protocol_fee_rate = whirlpool.protocol_fee_rate; 44 | let next_reward_infos = next_whirlpool_reward_infos(whirlpool, timestamp)?; 45 | 46 | let mut amount_remaining: u64 = amount; 47 | let mut amount_calculated: u64 = 0; 48 | let mut curr_sqrt_price = whirlpool.sqrt_price; 49 | let mut curr_tick_index = whirlpool.tick_current_index; 50 | let mut curr_liquidity = whirlpool.liquidity; 51 | let mut curr_protocol_fee: u64 = 0; 52 | let mut curr_array_index: usize = 0; 53 | let mut curr_fee_growth_global_input = if a_to_b { 54 | whirlpool.fee_growth_global_a 55 | } else { 56 | whirlpool.fee_growth_global_b 57 | }; 58 | 59 | while amount_remaining > 0 && adjusted_sqrt_price_limit != curr_sqrt_price { 60 | let (next_array_index, next_tick_index) = swap_tick_sequence 61 | .get_next_initialized_tick_index( 62 | curr_tick_index, 63 | tick_spacing, 64 | a_to_b, 65 | curr_array_index, 66 | )?; 67 | 68 | let (next_tick_sqrt_price, sqrt_price_target) = 69 | get_next_sqrt_prices(next_tick_index, adjusted_sqrt_price_limit, a_to_b); 70 | 71 | let swap_computation = compute_swap_step( 72 | curr_sqrt_price, 73 | sqrt_price_target, 74 | curr_liquidity, 75 | amount_remaining, 76 | fee_rate as u32, 77 | amount_specified_is_input, 78 | a_to_b, 79 | )?; 80 | 81 | if amount_specified_is_input { 82 | amount_remaining = amount_remaining 83 | .checked_sub(swap_computation.amount_in) 84 | .ok_or("ErrorCode::AmountRemainingOverflow")?; 85 | amount_remaining = amount_remaining 86 | .checked_sub(swap_computation.fee_amount) 87 | .ok_or("ErrorCode::AmountRemainingOverflow")?; 88 | 89 | amount_calculated = amount_calculated 90 | .checked_add(swap_computation.amount_out) 91 | .ok_or("ErrorCode::AmountCalcOverflow")?; 92 | } else { 93 | amount_remaining = amount_remaining 94 | .checked_sub(swap_computation.amount_out) 95 | .ok_or("ErrorCode::AmountRemainingOverflow")?; 96 | 97 | amount_calculated = amount_calculated 98 | .checked_add(swap_computation.amount_in) 99 | .ok_or("ErrorCode::AmountCalcOverflow")?; 100 | amount_calculated = amount_calculated 101 | .checked_add(swap_computation.fee_amount) 102 | .ok_or("ErrorCode::AmountCalcOverflow")?; 103 | } 104 | 105 | let (next_protocol_fee, next_fee_growth_global_input) = calculate_fees( 106 | swap_computation.fee_amount as u64, 107 | protocol_fee_rate, 108 | curr_liquidity, 109 | curr_protocol_fee, 110 | curr_fee_growth_global_input, 111 | ); 112 | curr_protocol_fee = next_protocol_fee; 113 | curr_fee_growth_global_input = next_fee_growth_global_input; 114 | 115 | if swap_computation.sqrt_price_next_x64 == next_tick_sqrt_price { 116 | let (next_tick, next_tick_initialized) = swap_tick_sequence 117 | .get_tick(next_array_index, next_tick_index, tick_spacing) 118 | .map_or_else(|_| (None, false), |tick| (Some(tick), tick.initialized)); 119 | 120 | if next_tick_initialized { 121 | let (fee_growth_global_a, fee_growth_global_b) = if a_to_b { 122 | (curr_fee_growth_global_input, whirlpool.fee_growth_global_b) 123 | } else { 124 | (whirlpool.fee_growth_global_a, curr_fee_growth_global_input) 125 | }; 126 | 127 | // todo: for test only 128 | let (update, next_liquidity) = calculate_update( 129 | next_tick.unwrap(), 130 | a_to_b, 131 | curr_liquidity, 132 | fee_growth_global_a, 133 | fee_growth_global_b, 134 | &next_reward_infos, 135 | )?; 136 | // let next_liquidity = calculate_update( 137 | // next_tick.unwrap(), 138 | // a_to_b, 139 | // curr_liquidity, 140 | // fee_growth_global_a, 141 | // fee_growth_global_b, 142 | // )?; 143 | 144 | curr_liquidity = next_liquidity; 145 | // todo: for test only 146 | swap_tick_sequence.update_tick( 147 | next_array_index, 148 | next_tick_index, 149 | tick_spacing, 150 | &update, 151 | )?; 152 | } 153 | 154 | let tick_offset = swap_tick_sequence.get_tick_offset( 155 | next_array_index, 156 | next_tick_index, 157 | tick_spacing, 158 | )?; 159 | 160 | curr_array_index = if (a_to_b && tick_offset == 0) 161 | || (!a_to_b && tick_offset == TICK_ARRAY_SIZE as isize - 1) 162 | { 163 | next_array_index + 1 164 | } else { 165 | next_array_index 166 | }; 167 | 168 | curr_tick_index = if a_to_b { 169 | next_tick_index - 1 170 | } else { 171 | next_tick_index 172 | }; 173 | } else if swap_computation.sqrt_price_next_x64 != curr_sqrt_price { 174 | curr_tick_index = tick_index_from_sqrt_price(&swap_computation.sqrt_price_next_x64); 175 | } 176 | 177 | curr_sqrt_price = swap_computation.sqrt_price_next_x64; 178 | } 179 | 180 | if amount_remaining > 0 && !amount_specified_is_input && sqrt_price_limit == NO_EXPLICIT_SQRT_PRICE_LIMIT { 181 | return Err("ErrorCode::PartialFillError"); 182 | } 183 | 184 | let (amount_a, amount_b) = if a_to_b == amount_specified_is_input { 185 | (amount - amount_remaining, amount_calculated) 186 | } else { 187 | (amount_calculated, amount - amount_remaining) 188 | }; 189 | 190 | Ok(PostSwapUpdate { 191 | amount_a, 192 | amount_b, 193 | next_liquidity: curr_liquidity, 194 | next_tick_index: curr_tick_index, 195 | next_sqrt_price: curr_sqrt_price, 196 | next_fee_growth_global: curr_fee_growth_global_input, 197 | next_reward_infos: [WhirlpoolRewardInfo::default(); 3], 198 | next_protocol_fee: curr_protocol_fee, 199 | }) 200 | } 201 | 202 | fn get_next_sqrt_prices( 203 | next_tick_index: i32, 204 | sqrt_price_limit: u128, 205 | a_to_b: bool, 206 | ) -> (u128, u128) { 207 | let next_tick_price = sqrt_price_from_tick_index(next_tick_index); 208 | let next_sqrt_price_limit = if a_to_b { 209 | sqrt_price_limit.max(next_tick_price) 210 | } else { 211 | sqrt_price_limit.min(next_tick_price) 212 | }; 213 | (next_tick_price, next_sqrt_price_limit) 214 | } 215 | 216 | // todo: for test only 217 | fn calculate_update( 218 | tick: &Tick, 219 | a_to_b: bool, 220 | liquidity: u128, 221 | fee_growth_global_a: u128, 222 | fee_growth_global_b: u128, 223 | reward_infos: &[WhirlpoolRewardInfo; NUM_REWARDS], 224 | ) -> Result<(TickUpdate, u128), &'static str> { 225 | // ) -> Result { 226 | let signed_liquidity_net = if a_to_b { 227 | -tick.liquidity_net 228 | } else { 229 | tick.liquidity_net 230 | }; 231 | 232 | let update = 233 | next_tick_cross_update(tick, fee_growth_global_a, fee_growth_global_b, reward_infos)?; 234 | 235 | // Update the global liquidity to reflect the new current tick 236 | // let next_liquidity = add_liquidity_delta(liquidity, signed_liquidity_net)?; 237 | let next_liquidity = add_delta(liquidity, signed_liquidity_net)?; 238 | 239 | Ok((update, next_liquidity)) 240 | // Ok(next_liquidity) 241 | } 242 | 243 | fn calculate_fees( 244 | fee_amount: u64, 245 | protocol_fee_rate: u16, 246 | curr_liquidity: u128, 247 | curr_protocol_fee: u64, 248 | curr_fee_growth_global_input: u128, 249 | ) -> (u64, u128) { 250 | let mut next_protocol_fee = curr_protocol_fee; 251 | let mut next_fee_growth_global_input = curr_fee_growth_global_input; 252 | let mut global_fee = fee_amount; 253 | if protocol_fee_rate > 0 { 254 | let delta = calculate_protocol_fee(global_fee, protocol_fee_rate); 255 | global_fee -= delta; 256 | next_protocol_fee = next_protocol_fee.wrapping_add(delta); 257 | } 258 | 259 | if curr_liquidity > 0 { 260 | next_fee_growth_global_input = next_fee_growth_global_input 261 | .wrapping_add(((global_fee as u128) << Q64_RESOLUTION) / curr_liquidity); 262 | } 263 | (next_protocol_fee, next_fee_growth_global_input) 264 | } 265 | 266 | fn calculate_protocol_fee(global_fee: u64, protocol_fee_rate: u16) -> u64 { 267 | ((global_fee as u128) * (protocol_fee_rate as u128) / PROTOCOL_FEE_RATE_MUL_VALUE) 268 | .try_into() 269 | .unwrap() 270 | } 271 | 272 | pub fn next_whirlpool_reward_infos( 273 | whirlpool: &OrcaClmmMarket, 274 | next_timestamp: u64, 275 | ) -> Result<[WhirlpoolRewardInfo; NUM_REWARDS], &'static str> { 276 | let curr_timestamp = whirlpool.reward_last_updated_timestamp; 277 | if next_timestamp < curr_timestamp { 278 | return Err("ErrorCode::InvalidTimestamp"); 279 | } 280 | 281 | if whirlpool.liquidity == 0 || next_timestamp == curr_timestamp { 282 | return Ok(whirlpool.reward_infos); 283 | } 284 | 285 | let mut next_reward_infos = whirlpool.reward_infos; 286 | let time_delta = u128::from(next_timestamp - curr_timestamp); 287 | for reward_info in next_reward_infos.iter_mut() { 288 | if !reward_info.initialized() { 289 | continue; 290 | } 291 | 292 | let reward_growth_delta = checked_mul_div( 293 | time_delta, 294 | reward_info.emissions_per_second_x64, 295 | whirlpool.liquidity, 296 | ) 297 | .unwrap_or(0); 298 | 299 | let curr_growth_global = reward_info.growth_global_x64; 300 | reward_info.growth_global_x64 = curr_growth_global.wrapping_add(reward_growth_delta); 301 | } 302 | 303 | Ok(next_reward_infos) 304 | } -------------------------------------------------------------------------------- /src/formula/raydium_clmm.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::ops::Neg; 3 | 4 | use crate::formula::clmm::concentrated_liquidity::compute_swap_step; 5 | use crate::formula::clmm::constant::{FEE_RATE_DENOMINATOR_VALUE, MAX_SQRT_PRICE_X64, MAX_TICK, MIN_SQRT_PRICE_X64, MIN_TICK}; 6 | use crate::formula::clmm::raydium_swap_state::{add_delta, StepComputations, SwapState}; 7 | use crate::formula::clmm::raydium_tick_array::{TickArrayBitmapExtension, TickArrayState, TickState}; 8 | use crate::formula::clmm::raydium_tick_math::{get_sqrt_price_at_tick, get_tick_at_sqrt_price}; 9 | use crate::formula::clmm::u256_math::U128; 10 | use crate::r#struct::pools::{AmmConfig, RaydiumClmmMarket}; 11 | 12 | pub fn swap_internal( 13 | amm_config: &AmmConfig, 14 | pool_state: &mut RaydiumClmmMarket, 15 | tick_array_states: &mut VecDeque, 16 | tick_array_bitmap_extension: &Option<&TickArrayBitmapExtension>, 17 | amount_specified: u64, 18 | sqrt_price_limit_x64: u128, 19 | zero_for_one: bool, 20 | is_base_input: bool, 21 | ) -> Result<(u64, u64), &'static str> { 22 | if amount_specified == 0u64 { 23 | return Err("Zero amount specified") 24 | } 25 | 26 | let sqrt_price_limit_x64 = if sqrt_price_limit_x64 == 0 { 27 | if zero_for_one { 28 | MIN_SQRT_PRICE_X64 + 1 29 | } else { 30 | MAX_SQRT_PRICE_X64 - 1 31 | } 32 | } else { 33 | sqrt_price_limit_x64 34 | }; 35 | 36 | if zero_for_one { 37 | if sqrt_price_limit_x64 < MIN_SQRT_PRICE_X64 { 38 | return Err("sqrt_price_limit_x64 must greater than MIN_SQRT_PRICE_X64"); 39 | } 40 | if sqrt_price_limit_x64 >= pool_state.sqrt_price_x64 { 41 | return Err("sqrt_price_limit_x64 must smaller than current"); 42 | } 43 | } else { 44 | if sqrt_price_limit_x64 > MAX_SQRT_PRICE_X64 { 45 | return Err("sqrt_price_limit_x64 must smaller than MAX_SQRT_PRICE_X64"); 46 | } 47 | if sqrt_price_limit_x64 <= pool_state.sqrt_price_x64 { 48 | return Err("sqrt_price_limit_x64 must greater than current"); 49 | } 50 | } 51 | 52 | let liquidity_start = pool_state.liquidity; 53 | 54 | let mut state = SwapState { 55 | amount_specified_remaining: amount_specified, 56 | amount_calculated: 0, 57 | sqrt_price_x64: pool_state.sqrt_price_x64, 58 | tick: pool_state.tick_current, 59 | // fee_growth_global_x64: if zero_for_one { 60 | // pool_state.fee_growth_global_0_x64 61 | // } else { 62 | // pool_state.fee_growth_global_1_x64 63 | // }, 64 | fee_amount: 0, 65 | protocol_fee: 0, 66 | fund_fee: 0, 67 | liquidity: liquidity_start, 68 | }; 69 | 70 | let (mut is_match_pool_current_tick_array, first_valid_tick_array_start_index) = 71 | pool_state.get_first_initialized_tick_array(tick_array_bitmap_extension, zero_for_one)?; 72 | let mut current_valid_tick_array_start_index = first_valid_tick_array_start_index; 73 | 74 | let mut tick_array_current = tick_array_states.pop_front().unwrap(); 75 | 76 | for _ in 0..tick_array_states.len() { 77 | if tick_array_current.start_tick_index == current_valid_tick_array_start_index { 78 | break; 79 | } 80 | tick_array_current = tick_array_states 81 | .pop_front() 82 | .ok_or("not enough tick array account")?; 83 | } 84 | 85 | if tick_array_current.start_tick_index != current_valid_tick_array_start_index { 86 | return Err("invalid first tick array account") 87 | } 88 | 89 | /////////////////////////////////////// start of while loop 90 | while state.amount_specified_remaining != 0 91 | && state.sqrt_price_x64 != sqrt_price_limit_x64 92 | && state.tick < MAX_TICK 93 | && state.tick > MIN_TICK 94 | { 95 | let mut step = StepComputations::default(); 96 | step.sqrt_price_start_x64 = state.sqrt_price_x64; 97 | 98 | let mut next_initialized_tick = if let Some(tick_state) = tick_array_current 99 | .next_initialized_tick(state.tick, pool_state.tick_spacing, zero_for_one)? 100 | { 101 | Box::new(*tick_state) 102 | } else { 103 | if !is_match_pool_current_tick_array { 104 | is_match_pool_current_tick_array = true; 105 | Box::new(tick_array_current.first_initialized_tick(zero_for_one)?.clone()) 106 | } else { 107 | Box::new(TickState::default()) 108 | } 109 | }; 110 | 111 | if !next_initialized_tick.is_initialized() { 112 | let next_initialized_tick_array_index = pool_state 113 | .next_initialized_tick_array_start_index( 114 | tick_array_bitmap_extension, 115 | current_valid_tick_array_start_index, 116 | zero_for_one, 117 | )?; 118 | if next_initialized_tick_array_index.is_none() { 119 | return Err("liquidity insufficient"); 120 | } 121 | 122 | while tick_array_current.start_tick_index != next_initialized_tick_array_index.unwrap() { 123 | tick_array_current = tick_array_states 124 | .pop_front() 125 | .ok_or("not enough tick array account")?; 126 | } 127 | current_valid_tick_array_start_index = next_initialized_tick_array_index.unwrap(); 128 | 129 | let first_initialized_tick = tick_array_current.first_initialized_tick(zero_for_one)?; 130 | next_initialized_tick = Box::new(first_initialized_tick.clone()); 131 | } 132 | step.tick_next = next_initialized_tick.tick; 133 | step.initialized = next_initialized_tick.is_initialized(); 134 | 135 | if step.tick_next < MIN_TICK { 136 | step.tick_next = MIN_TICK; 137 | } else if step.tick_next > MAX_TICK { 138 | step.tick_next = MAX_TICK; 139 | } 140 | step.sqrt_price_next_x64 = get_sqrt_price_at_tick(step.tick_next)?; 141 | 142 | let target_price = if (zero_for_one && step.sqrt_price_next_x64 < sqrt_price_limit_x64) 143 | || (!zero_for_one && step.sqrt_price_next_x64 > sqrt_price_limit_x64) 144 | { 145 | sqrt_price_limit_x64 146 | } else { 147 | step.sqrt_price_next_x64 148 | }; 149 | 150 | let swap_step = compute_swap_step( 151 | step.sqrt_price_start_x64, 152 | target_price, 153 | state.liquidity, 154 | state.amount_specified_remaining, 155 | amm_config.trade_fee_rate, 156 | is_base_input, 157 | zero_for_one 158 | )?; 159 | 160 | // if zero_for_one { 161 | // if swap_step.sqrt_price_next_x64 < target_price { 162 | // return Err("invalid result") 163 | // } 164 | // } else { 165 | // if target_price < swap_step.sqrt_price_next_x64 { 166 | // return Err("invalid result") 167 | // } 168 | // } 169 | 170 | state.sqrt_price_x64 = swap_step.sqrt_price_next_x64; 171 | step.amount_in = swap_step.amount_in; 172 | step.amount_out = swap_step.amount_out; 173 | step.fee_amount = swap_step.fee_amount; 174 | 175 | if is_base_input { 176 | state.amount_specified_remaining = state 177 | .amount_specified_remaining 178 | .checked_sub(step.amount_in + step.fee_amount) 179 | .unwrap(); 180 | state.amount_calculated = state 181 | .amount_calculated 182 | .checked_add(step.amount_out) 183 | .unwrap(); 184 | } else { 185 | state.amount_specified_remaining = state 186 | .amount_specified_remaining 187 | .checked_sub(step.amount_out) 188 | .unwrap(); 189 | 190 | let step_amount_calculate = step 191 | .amount_in 192 | .checked_add(step.fee_amount) 193 | .expect("calculate overflow"); 194 | state.amount_calculated = state 195 | .amount_calculated 196 | .checked_add(step_amount_calculate) 197 | .expect("calculate overflow"); 198 | } 199 | 200 | //////////////////// todo: for test only 201 | let step_fee_amount = step.fee_amount; 202 | 203 | if amm_config.protocol_fee_rate > 0 { 204 | let delta = u64::from(step_fee_amount) 205 | .checked_mul(u64::from(amm_config.protocol_fee_rate)) 206 | .unwrap() 207 | .checked_div(u64::from(FEE_RATE_DENOMINATOR_VALUE)) 208 | .unwrap(); 209 | step.fee_amount = step.fee_amount.checked_sub(delta).unwrap(); 210 | state.protocol_fee = state.protocol_fee.checked_add(delta).unwrap(); 211 | } 212 | 213 | if amm_config.fund_fee_rate > 0 { 214 | let delta = u64::from(step_fee_amount) 215 | .checked_mul(u64::from(amm_config.fund_fee_rate)) 216 | .unwrap() 217 | .checked_div(u64::from(FEE_RATE_DENOMINATOR_VALUE)) 218 | .unwrap(); 219 | step.fee_amount = step.fee_amount.checked_sub(delta).unwrap(); 220 | state.fund_fee = state.fund_fee.checked_add(delta).unwrap(); 221 | } 222 | 223 | if state.liquidity > 0 { 224 | // todo 225 | // let fee_growth_global_x64_delta = U128::from(step.fee_amount) 226 | // .mul_div_floor(Q64, U128::from(state.liquidity)) 227 | // .unwrap() 228 | // .as_u128(); 229 | 230 | // let fee_growth_global_x64_delta = BigFloat::from(step.fee_amount) 231 | // .mul(&BigFloat::from(Q64)) 232 | // .div(&BigFloat::from(state.liquidity)) 233 | // .floor() 234 | // .to_u128() 235 | // .unwrap(); 236 | 237 | // state.fee_growth_global_x64 = state 238 | // .fee_growth_global_x64 239 | // .checked_add(fee_growth_global_x64_delta) 240 | // .unwrap(); 241 | state.fee_amount = state.fee_amount.checked_add(step.fee_amount).unwrap(); 242 | } 243 | //////////////////// todo: for test only 244 | 245 | if state.sqrt_price_x64 == step.sqrt_price_next_x64 { 246 | if step.initialized { 247 | let mut liquidity_net = next_initialized_tick.liquidity_net; 248 | //////////////////// todo: for test only 249 | tick_array_current.update_tick_state( 250 | next_initialized_tick.tick, 251 | pool_state.tick_spacing.into(), 252 | *next_initialized_tick, 253 | )?; 254 | //////////////////// todo: for test only 255 | 256 | if zero_for_one { 257 | liquidity_net = liquidity_net.neg(); 258 | } 259 | state.liquidity = add_delta(state.liquidity, liquidity_net)?; 260 | } 261 | 262 | state.tick = if zero_for_one { 263 | step.tick_next - 1 264 | } else { 265 | step.tick_next 266 | }; 267 | } else if state.sqrt_price_x64 != step.sqrt_price_start_x64 { 268 | state.tick = get_tick_at_sqrt_price(state.sqrt_price_x64)?; 269 | } 270 | } 271 | /////////////////////////////////////// end of while loop 272 | 273 | //////////////////// todo: for test only 274 | if state.tick != pool_state.tick_current { 275 | pool_state.tick_current = state.tick; 276 | } 277 | 278 | pool_state.sqrt_price_x64 = state.sqrt_price_x64; 279 | 280 | if liquidity_start != state.liquidity { 281 | pool_state.liquidity = state.liquidity; 282 | } 283 | //////////////////// todo: for test only 284 | 285 | let (amount_0, amount_1) = if zero_for_one == is_base_input { 286 | ( 287 | amount_specified 288 | .checked_sub(state.amount_specified_remaining) 289 | .unwrap(), 290 | state.amount_calculated, 291 | ) 292 | } else { 293 | ( 294 | state.amount_calculated, 295 | amount_specified 296 | .checked_sub(state.amount_specified_remaining) 297 | .unwrap(), 298 | ) 299 | }; 300 | 301 | //////////////////// todo: for test only 302 | if zero_for_one { 303 | // pool_state.fee_growth_global_0_x64 = state.fee_growth_global_x64; 304 | pool_state.total_fees_token_0 = pool_state 305 | .total_fees_token_0 306 | .checked_add(state.fee_amount as u64) 307 | .unwrap(); 308 | 309 | if state.protocol_fee > 0 { 310 | pool_state.protocol_fees_token_0 = pool_state 311 | .protocol_fees_token_0 312 | .checked_add(state.protocol_fee as u64) 313 | .unwrap(); 314 | } 315 | if state.fund_fee > 0 { 316 | pool_state.fund_fees_token_0 = pool_state 317 | .fund_fees_token_0 318 | .checked_add(state.fund_fee as u64) 319 | .unwrap(); 320 | } 321 | pool_state.swap_in_amount_token_0 = pool_state 322 | .swap_in_amount_token_0 323 | .checked_add(u128::from(amount_0)) 324 | .unwrap(); 325 | pool_state.swap_out_amount_token_1 = pool_state 326 | .swap_out_amount_token_1 327 | .checked_add(u128::from(amount_1)) 328 | .unwrap(); 329 | } else { 330 | // pool_state.fee_growth_global_1_x64 = state.fee_growth_global_x64; 331 | pool_state.total_fees_token_1 = pool_state 332 | .total_fees_token_1 333 | .checked_add(state.fee_amount as u64) 334 | .unwrap(); 335 | 336 | if state.protocol_fee > 0 { 337 | pool_state.protocol_fees_token_1 = pool_state 338 | .protocol_fees_token_1 339 | .checked_add(state.protocol_fee as u64) 340 | .unwrap(); 341 | } 342 | if state.fund_fee > 0 { 343 | pool_state.fund_fees_token_1 = pool_state 344 | .fund_fees_token_1 345 | .checked_add(state.fund_fee as u64) 346 | .unwrap(); 347 | } 348 | pool_state.swap_in_amount_token_1 = pool_state 349 | .swap_in_amount_token_1 350 | .checked_add(u128::from(amount_1)) 351 | .unwrap(); 352 | pool_state.swap_out_amount_token_0 = pool_state 353 | .swap_out_amount_token_0 354 | .checked_add(u128::from(amount_0)) 355 | .unwrap(); 356 | } 357 | //////////////////// todo: for test only 358 | 359 | Ok((amount_0 as u64, amount_1 as u64)) 360 | } -------------------------------------------------------------------------------- /src/formula/raydium_openbook.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey::Pubkey; 2 | use crate::formula::clmm::u256_math::U128; 3 | use crate::formula::openbook::math::Calculator; 4 | use crate::formula::openbook::openbook_processor::{LeafNode, SwapDirection}; 5 | 6 | pub fn process_swap_base_in( 7 | program_id: &Pubkey, 8 | accounts: &[AccountInfo], 9 | swap: SwapInstructionBaseIn, 10 | ) -> Result<(), &'static str> { 11 | const ACCOUNT_LEN: usize = 17; 12 | let input_account_len = accounts.len(); 13 | if input_account_len != ACCOUNT_LEN && input_account_len != ACCOUNT_LEN + 1 { 14 | return Err("AmmError::WrongAccountsNumber"); 15 | } 16 | let account_info_iter = &mut accounts.iter(); 17 | let token_program_info = next_account_info(account_info_iter)?; 18 | 19 | let amm_info = next_account_info(account_info_iter)?; 20 | let amm_authority_info = next_account_info(account_info_iter)?; 21 | let amm_open_orders_info = next_account_info(account_info_iter)?; 22 | if input_account_len == ACCOUNT_LEN + 1 { 23 | let _amm_target_orders_info = next_account_info(account_info_iter)?; 24 | } 25 | let amm_coin_vault_info = next_account_info(account_info_iter)?; 26 | let amm_pc_vault_info = next_account_info(account_info_iter)?; 27 | 28 | let market_program_info = next_account_info(account_info_iter)?; 29 | 30 | let mut amm = AmmInfo::load_mut_checked(&amm_info, program_id)?; 31 | let enable_orderbook; 32 | if AmmStatus::from_u64(amm.status).orderbook_permission() { 33 | enable_orderbook = true; 34 | } else { 35 | enable_orderbook = false; 36 | } 37 | let market_info = next_account_info(account_info_iter)?; 38 | let market_bids_info = next_account_info(account_info_iter)?; 39 | let market_asks_info = next_account_info(account_info_iter)?; 40 | let market_event_queue_info = next_account_info(account_info_iter)?; 41 | let market_coin_vault_info = next_account_info(account_info_iter)?; 42 | let market_pc_vault_info = next_account_info(account_info_iter)?; 43 | let market_vault_signer = next_account_info(account_info_iter)?; 44 | 45 | let user_source_info = next_account_info(account_info_iter)?; 46 | let user_destination_info = next_account_info(account_info_iter)?; 47 | let user_source_owner = next_account_info(account_info_iter)?; 48 | if !user_source_owner.is_signer { 49 | return Err(AmmError::InvalidSignAccount.into()); 50 | } 51 | check_assert_eq!( 52 | *token_program_info.key, 53 | spl_token::id(), 54 | "spl_token_program", 55 | AmmError::InvalidSplTokenProgram 56 | ); 57 | let spl_token_program_id = token_program_info.key; 58 | if *amm_authority_info.key 59 | != Self::authority_id(program_id, AUTHORITY_AMM, amm.nonce as u8)? 60 | { 61 | return Err("AmmError::InvalidProgramAddress"); 62 | } 63 | check_assert_eq!( 64 | *amm_coin_vault_info.key, 65 | amm.coin_vault, 66 | "coin_vault", 67 | AmmError::InvalidCoinVault 68 | ); 69 | check_assert_eq!( 70 | *amm_pc_vault_info.key, 71 | amm.pc_vault, 72 | "pc_vault", 73 | AmmError::InvalidPCVault 74 | ); 75 | 76 | if *user_source_info.key == amm.pc_vault || *user_source_info.key == amm.coin_vault { 77 | return Err("AmmError::InvalidUserToken"); 78 | } 79 | if *user_destination_info.key == amm.pc_vault 80 | || *user_destination_info.key == amm.coin_vault 81 | { 82 | return Err("AmmError::InvalidUserToken"); 83 | } 84 | 85 | let amm_coin_vault = 86 | Self::unpack_token_account(&amm_coin_vault_info, spl_token_program_id)?; 87 | let amm_pc_vault = Self::unpack_token_account(&amm_pc_vault_info, spl_token_program_id)?; 88 | 89 | let user_source = Self::unpack_token_account(&user_source_info, spl_token_program_id)?; 90 | let user_destination = 91 | Self::unpack_token_account(&user_destination_info, spl_token_program_id)?; 92 | 93 | if !AmmStatus::from_u64(amm.status).swap_permission() { 94 | msg!(&format!("swap_base_in: status {}", amm.status)); 95 | let clock = Clock::get()?; 96 | if amm.status == AmmStatus::OrderBookOnly.into_u64() 97 | && (clock.unix_timestamp as u64) >= amm.state_data.orderbook_to_init_time 98 | { 99 | amm.status = AmmStatus::Initialized.into_u64(); 100 | msg!("swap_base_in: OrderBook to Initialized"); 101 | } else { 102 | return Err("AmmError::InvalidStatus"); 103 | } 104 | } else if amm.status == AmmStatus::WaitingTrade.into_u64() { 105 | let clock = Clock::get()?; 106 | if (clock.unix_timestamp as u64) < amm.state_data.pool_open_time { 107 | return Err("AmmError::InvalidStatus"); 108 | } else { 109 | amm.status = AmmStatus::SwapOnly.into_u64(); 110 | println!("swap_base_in: WaitingTrade to SwapOnly"); 111 | } 112 | } 113 | 114 | let total_pc_without_take_pnl; 115 | let total_coin_without_take_pnl; 116 | let mut bids: Vec = Vec::new(); 117 | let mut asks: Vec = Vec::new(); 118 | if enable_orderbook { 119 | check_assert_eq!( 120 | *amm_open_orders_info.key, 121 | amm.open_orders, 122 | "open_orders", 123 | AmmError::InvalidOpenOrders 124 | ); 125 | check_assert_eq!( 126 | *market_program_info.key, 127 | amm.market_program, 128 | "market_program", 129 | AmmError::InvalidMarketProgram 130 | ); 131 | check_assert_eq!( 132 | *market_info.key, 133 | amm.market, 134 | "market", 135 | AmmError::InvalidMarket 136 | ); 137 | let (market_state, open_orders) = Processor::load_serum_market_order( 138 | market_info, 139 | amm_open_orders_info, 140 | amm_authority_info, 141 | &amm, 142 | false, 143 | )?; 144 | let bids_orders = market_state.load_bids_mut(&market_bids_info)?; 145 | let asks_orders = market_state.load_asks_mut(&market_asks_info)?; 146 | (bids, asks) = Self::get_amm_orders(&open_orders, bids_orders, asks_orders)?; 147 | (total_pc_without_take_pnl, total_coin_without_take_pnl) = 148 | Calculator::calc_total_without_take_pnl( 149 | amm_pc_vault.amount, 150 | amm_coin_vault.amount, 151 | &open_orders, 152 | &amm, 153 | &market_state, 154 | &market_event_queue_info, 155 | &amm_open_orders_info, 156 | )?; 157 | } else { 158 | (total_pc_without_take_pnl, total_coin_without_take_pnl) = 159 | Calculator::calc_total_without_take_pnl_no_orderbook( 160 | amm_pc_vault.amount, 161 | amm_coin_vault.amount, 162 | &amm, 163 | )?; 164 | } 165 | 166 | let swap_direction; 167 | if user_source.mint == amm_coin_vault.mint && user_destination.mint == amm_pc_vault.mint { 168 | swap_direction = SwapDirection::Coin2PC 169 | } else if user_source.mint == amm_pc_vault.mint 170 | && user_destination.mint == amm_coin_vault.mint 171 | { 172 | swap_direction = SwapDirection::PC2Coin 173 | } else { 174 | return Err("AmmError::InvalidUserToken"); 175 | } 176 | if user_source.amount < swap.amount_in { 177 | // encode_ray_log(SwapBaseInLog { 178 | // log_type: LogType::SwapBaseIn.into_u8(), 179 | // amount_in: swap.amount_in, 180 | // minimum_out: swap.minimum_amount_out, 181 | // direction: swap_direction as u64, 182 | // user_source: user_source.amount, 183 | // pool_coin: total_coin_without_take_pnl, 184 | // pool_pc: total_pc_without_take_pnl, 185 | // out_amount: 0, 186 | // }); 187 | return Err("AmmError::InsufficientFunds"); 188 | } 189 | let swap_fee = U128::from(swap.amount_in) 190 | .checked_mul(amm.fees.swap_fee_numerator.into()) 191 | .unwrap() 192 | .checked_ceil_div(amm.fees.swap_fee_denominator.into()) 193 | .unwrap() 194 | .0; 195 | let swap_in_after_deduct_fee = U128::from(swap.amount_in).checked_sub(swap_fee).unwrap(); 196 | let swap_amount_out = Calculator::swap_token_amount_base_in( 197 | swap_in_after_deduct_fee, 198 | total_pc_without_take_pnl.into(), 199 | total_coin_without_take_pnl.into(), 200 | swap_direction, 201 | ) 202 | .as_u64(); 203 | // encode_ray_log(SwapBaseInLog { 204 | // log_type: LogType::SwapBaseIn.into_u8(), 205 | // amount_in: swap.amount_in, 206 | // minimum_out: swap.minimum_amount_out, 207 | // direction: swap_direction as u64, 208 | // user_source: user_source.amount, 209 | // pool_coin: total_coin_without_take_pnl, 210 | // pool_pc: total_pc_without_take_pnl, 211 | // out_amount: swap_amount_out, 212 | // }); 213 | if swap_amount_out < swap.minimum_amount_out { 214 | return Err("AmmError::ExceededSlippage"); 215 | } 216 | if swap_amount_out == 0 || swap.amount_in == 0 { 217 | return Err("AmmError::InvalidInput"); 218 | } 219 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate core; 2 | 3 | use std::cmp::PartialEq; 4 | use std::collections::HashMap; 5 | use std::str::FromStr; 6 | use std::sync::{Arc, Mutex}; 7 | use std::time::Duration; 8 | use log::debug; 9 | 10 | use solana_client::rpc_client::RpcClient; 11 | use solana_sdk::pubkey::Pubkey; 12 | use tokio::spawn; 13 | use tokio::sync::broadcast; 14 | use tokio::time::sleep; 15 | 16 | use path::PathFinder; 17 | 18 | use crate::observer::Event; 19 | use crate::probe::Probe; 20 | use crate::r#struct::account::{DeserializedAccount, DeserializedPoolAccount}; 21 | use crate::r#struct::market::Market; 22 | use crate::utils::read_pools; 23 | 24 | mod probe; 25 | mod constants; 26 | mod utils; 27 | mod formula; 28 | pub mod path; 29 | mod r#struct; 30 | mod temp; 31 | mod arbitrageur; 32 | mod observer; 33 | 34 | #[tokio::main] 35 | async fn main() { 36 | let alchemy = "https://solana-mainnet.g.alchemy.com/v2/76-rZCjoPGCHXLfjHNojk5CiqX8I36AT".to_string(); 37 | // let get_blocks = "https://go.getblock.io/bd8eab2bbe6e448b84ca2ae3b282b819".to_string(); 38 | let rpc_url = alchemy; 39 | let rpc_client = RpcClient::new(rpc_url.clone()); 40 | 41 | // read pools 42 | let orca_pools = read_pools("./src/pubkey/orca.json").unwrap(); 43 | let raydium_pools = read_pools("./src/pubkey/raydium.json").unwrap(); 44 | let meteora_pools = read_pools("./src/pubkey/meteora.json").unwrap(); 45 | 46 | // concatenate all dex pools 47 | let pool_list = Arc::new(Mutex::new(HashMap::from([ 48 | (Market::ORCA, orca_pools), 49 | (Market::RAYDIUM, raydium_pools), 50 | // (Market::METEORA, meteora_pools), 51 | ]))); 52 | 53 | // hold pool pubkey 54 | let pool_account_bin: Arc>> = Arc::new(Mutex::new(Vec::new())); 55 | // hold pubkey in data or pda 56 | let shared_account_bin: Arc>> = Arc::new(Mutex::new(Vec::new())); 57 | // hold available path list of mint 58 | let path_list: Arc>>> = Arc::new(Mutex::new(HashMap::new())); 59 | 60 | let mut probe = Probe::new(rpc_url.clone()); 61 | // fetch pool pubkeys 62 | probe.fetch_pool_accounts(Arc::clone(&pool_list), Arc::clone(&pool_account_bin)); 63 | 64 | // resolve path 65 | let pool_accounts = Arc::clone(&pool_account_bin); 66 | let path_list = Arc::clone(&path_list); 67 | 68 | let path_finder = PathFinder { 69 | pool_accounts: Arc::clone(&pool_accounts), 70 | path_list: Arc::clone(&path_list) 71 | }; 72 | 73 | let mint = Pubkey::from_str("So11111111111111111111111111111111111111112").unwrap(); 74 | path_finder.resolve_path(mint); 75 | 76 | let (tx, mut rx) = broadcast::channel(10); 77 | tx.send(Event::Initialized).expect("broadcast: failed to broadcast"); 78 | 79 | ////////////////////////////////////////////////////////////////////////////////////////////////////////// 80 | 81 | // collect swap-related pubkeys from pool accounts 82 | probe.start_watching(Arc::clone(&pool_account_bin), Arc::clone(&shared_account_bin), tx.clone()); 83 | 84 | // setup and run arbitrage 85 | let shared_pool_account_bin = Arc::clone(&shared_account_bin); 86 | let path_list = Arc::clone(&path_list); 87 | let rpc_client = RpcClient::new(rpc_url.clone()); 88 | let mut rx1 = tx.subscribe(); 89 | spawn(async move { 90 | loop { 91 | if !rx1.is_empty() { 92 | match rx1.recv().await { 93 | Ok(event) => { 94 | debug!("broadcast: event received: {:?}", event); 95 | match event { 96 | Event::Initialized => {} 97 | Event::UpdateAccounts => { 98 | let path_list = path_list.lock().unwrap().clone(); 99 | 100 | let target = path_list.iter().find(|path| { 101 | *path.0 == mint 102 | }).expect(format!("path: path not found for mint: {}", mint).as_str()); 103 | 104 | target.1.iter().for_each(|pool| { 105 | let related_pubkeys = pool.get_swap_related_pubkeys(Some(&rpc_client)).unwrap(); 106 | 107 | let related_accounts = shared_pool_account_bin.lock().unwrap().clone().into_iter().filter(|account| { 108 | related_pubkeys.iter().find(|(_, pubkey)| { 109 | *pubkey == account.get_pubkey() 110 | }).is_some() 111 | }).collect::>(); 112 | 113 | target.1.iter().for_each(|pool| { 114 | pool.operation.swap(&related_accounts); 115 | }); 116 | }); 117 | } 118 | } 119 | } 120 | Err(_) => { 121 | eprintln!("broadcast: nothing to receive"); 122 | } 123 | } 124 | } 125 | 126 | sleep(Duration::from_secs(1)).await; 127 | } 128 | }); 129 | 130 | // spawn(async move { 131 | // loop { 132 | // let path_list = path_list.lock().unwrap().clone(); 133 | // 134 | // let target = path_list.iter().find(|path| { 135 | // *path.0 == mint 136 | // }).expect(format!("no path for mint: {}", mint).as_str()); 137 | // 138 | // target.1.iter().for_each(|pool| { 139 | // let related_pubkeys = pool.get_swap_related_pubkeys(Some(&rpc_client)).unwrap(); 140 | // 141 | // let related_accounts = shared_pool_account_bin.lock().unwrap().clone().into_iter().filter(|account| { 142 | // related_pubkeys.iter().find(|(_, pubkey)| { 143 | // *pubkey == account.get_pubkey() 144 | // }).is_some() 145 | // }).collect::>(); 146 | // 147 | // // target.1.iter().for_each(|pool| { 148 | // // pool.operation.swap(&related_accounts); 149 | // // }); 150 | // }); 151 | // 152 | // // single swap test 153 | // // if let Ok(related_pubkeys) = target.1.iter().find(|pool| { 154 | // // pool.market == RAYDIUM && pool.operation.get_formula() == Formula::ConcentratedLiquidity 155 | // // }).unwrap().get_swap_related_pubkeys(Some(&rpc_client)) { 156 | // // let related_accounts = shared_pool_account_bin.lock().unwrap().clone().into_iter().filter(|account| { 157 | // // related_pubkeys.iter().find(|(_, pubkey)| { 158 | // // *pubkey == account.get_pubkey() 159 | // // }).is_some() 160 | // // }).collect::>(); 161 | // // 162 | // // if let Some(target_pool) = target.1.iter().find(|pool| { 163 | // // pool.market == RAYDIUM && pool.operation.get_formula() == Formula::ConcentratedLiquidity 164 | // // }) { 165 | // // target_pool.operation.swap(&related_accounts); 166 | // // } 167 | // // } 168 | // 169 | // let _ = sleep(Duration::from_secs(5)).await; 170 | // } 171 | // }); 172 | 173 | // main thread loop 174 | loop { 175 | sleep(Duration::from_secs(1)).await; 176 | } 177 | } 178 | 179 | pub fn test() { 180 | 181 | } -------------------------------------------------------------------------------- /src/observer.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 5 | pub enum Event { 6 | Initialized, 7 | UpdateAccounts 8 | } 9 | 10 | pub type Subscriber = fn(); 11 | 12 | #[derive(Default, Clone)] 13 | pub struct Publisher { 14 | events: HashMap> 15 | } 16 | 17 | impl Publisher { 18 | pub fn subscribe(&mut self, event: Event, listener: Subscriber) { 19 | self.events.entry(event.clone()).or_default(); 20 | if let Some(events) = self.events.get_mut(&event) { 21 | events.push(listener); 22 | } 23 | } 24 | 25 | // pub fn unsubscribe(&mut self, event: Event, listener: Subscriber) { 26 | // self.events.get_mut(&event).unwrap().retain(|&subscriber| { 27 | // subscriber != listener 28 | // }) 29 | // } 30 | 31 | pub fn notify(&self, event: Event) { 32 | if let Some(listeners) = &self.events.get(&event) { 33 | listeners.iter().for_each(|&subscriber| { 34 | subscriber() 35 | }) 36 | } 37 | else { 38 | 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | use std::rc::Rc; 4 | use std::sync::{Arc, Mutex}; 5 | use solana_sdk::pubkey::Pubkey; 6 | use tokio::time::Instant; 7 | use crate::constants::MAX_DEPTH; 8 | use crate::r#struct::account::DeserializedPoolAccount; 9 | use crate::r#struct::market::Market; 10 | 11 | pub struct PathFinder { 12 | pub pool_accounts: Arc>>, 13 | pub path_list: Arc>>>, 14 | } 15 | 16 | impl PathFinder { 17 | pub fn resolve_path(&self, mint: Pubkey) { 18 | let t = Instant::now(); 19 | let path: Rc>> = Rc::new(RefCell::new(Vec::new())); 20 | let len = (*Arc::clone(&self.pool_accounts).lock().unwrap()).len(); 21 | for i in 2..len + 1 { 22 | Self::find_path( 23 | Arc::clone(&self.path_list), 24 | Arc::clone(&self.pool_accounts), 25 | Rc::clone(&path), 26 | 0, 27 | i, 28 | mint, 29 | mint 30 | ) 31 | } 32 | 33 | println!("path: path resolved ({:?})", t.elapsed()); 34 | } 35 | 36 | fn find_path( 37 | path_list: Arc>>>, 38 | pools: Arc>>, 39 | path: Rc>>, 40 | start: usize, 41 | r: usize, 42 | next_mint: Pubkey, 43 | target_mint: Pubkey 44 | ) { 45 | if r == 0 { 46 | let tmp_path = Rc::clone(&path); 47 | if Self::validate_path(&tmp_path, &target_mint) { 48 | // println!("[{}]", tmp_path.borrow().iter().map(|x| { 49 | // format!("({}({}) - ({}, {}))", x.market.name(), x.pubkey, x.operation.get_mint_pair().pubkey_a, x.operation.get_mint_pair().pubkey_b) 50 | // }).collect::>().join(",")); 51 | (*path_list.lock().unwrap()).insert(target_mint, tmp_path.take()); 52 | } 53 | return; 54 | } 55 | else { 56 | let tmp_path = Rc::clone(&path); 57 | let pools = Arc::clone(&pools); 58 | 59 | let len = (*pools.lock().unwrap()).len(); 60 | for i in start..len { 61 | let accounts = (*pools.lock().unwrap()).clone(); 62 | 63 | let account = accounts[i].clone(); 64 | let pair = account.operation.get_mint_pair(); 65 | if !pair.any(next_mint) || Self::contains_dex(&account.market, &tmp_path.borrow()) { 66 | continue; 67 | } 68 | 69 | tmp_path.borrow_mut().push(account.clone()); 70 | let next_mint = if pair.pubkey_a == next_mint { 71 | pair.pubkey_b 72 | } 73 | else { 74 | pair.pubkey_a 75 | }; 76 | 77 | Self::find_path(Arc::clone(&path_list), Arc::clone(&pools), Rc::clone(&path), i+1, r-1, next_mint, target_mint); 78 | tmp_path.borrow_mut().pop(); 79 | 80 | // basic 81 | // let account = accounts[i].clone(); 82 | // tmp_path.borrow_mut().push(account.clone()); 83 | // Self::find_path(Arc::clone(&path_list), Arc::clone(&pools), Rc::clone(&path), i+1, r-1, next_mint, target_mint); 84 | // tmp_path.borrow_mut().pop(); 85 | } 86 | } 87 | } 88 | 89 | fn validate_path(path: &Rc>>, target_mint: &Pubkey) -> bool { 90 | if MAX_DEPTH < path.borrow().len() { 91 | false 92 | } 93 | else { 94 | if path.borrow().iter().filter(|sub_path| { 95 | sub_path.operation.get_mint_pair().any(*target_mint) 96 | }).collect::>().len() == 2 { 97 | true 98 | } 99 | else { 100 | false 101 | } 102 | } 103 | } 104 | 105 | fn contains_dex(market: &Market, accounts: &Vec) -> bool { 106 | accounts.iter().find(|account| { 107 | account.market.eq(market) 108 | }).is_some() 109 | } 110 | } -------------------------------------------------------------------------------- /src/probe.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Debug; 3 | use std::sync::{Arc, Mutex}; 4 | use std::time::Duration; 5 | use log::debug; 6 | 7 | use solana_client::rpc_client::RpcClient; 8 | use solana_sdk::account::Account; 9 | use solana_sdk::pubkey::Pubkey; 10 | use tokio::spawn; 11 | use tokio::sync::broadcast::Sender; 12 | use tokio::time::{Instant, sleep}; 13 | 14 | use crate::observer::{Event}; 15 | use crate::r#struct::account::{AccountDataSerializer, DeserializedAccount, DeserializedDataAccount, DeserializedPoolAccount, DeserializedTokenAccount}; 16 | use crate::r#struct::market::Market; 17 | use crate::r#struct::resolver::{resolve_pool_account, resolve_pool_config_account}; 18 | use crate::r#struct::token::TokenAccount; 19 | 20 | pub struct Probe { 21 | pub rpc_url: String 22 | } 23 | 24 | impl Probe { 25 | pub fn new(rpc_url: String) -> Probe { 26 | Probe { 27 | rpc_url 28 | } 29 | } 30 | 31 | // fetch pool accounts one time 32 | pub fn fetch_pool_accounts( 33 | &self, 34 | pools: Arc>>>, 35 | pool_account_bin: Arc>> 36 | ) { 37 | let rpc_client: RpcClient = RpcClient::new(&self.rpc_url); 38 | 39 | debug!("probe: fetching market pools..."); 40 | let fetched_markets = pools.lock().unwrap().iter().map(|pools| { 41 | let accounts = Self::_fetch_accounts(&rpc_client, pools.1); 42 | 43 | let valid_accounts = accounts.iter().enumerate().filter(|(index, account)| { 44 | account.is_some() 45 | }).map(|(index, account)| { 46 | let account = account.clone().unwrap(); 47 | let data = account.data.clone(); 48 | 49 | let market_operation = resolve_pool_account(pools.0, &data); 50 | DeserializedPoolAccount { 51 | pubkey: (&*pools.1)[index], 52 | account, 53 | market: (*pools.0).clone(), 54 | operation: market_operation, 55 | } 56 | }).collect::>(); 57 | 58 | valid_accounts 59 | }).collect::>>().into_iter().flatten().collect::>(); 60 | 61 | *pool_account_bin.lock().unwrap() = fetched_markets; 62 | } 63 | 64 | // fetch accounts one time 65 | pub fn fetch_multiple_accounts( 66 | &self, 67 | items: Vec<(Market, DeserializedAccount, Pubkey)>, 68 | bin: Arc>> 69 | ) { 70 | let rpc_client = RpcClient::new(self.rpc_url.clone()); 71 | Self::_fetch_multiple_accounts(&rpc_client, items, bin, None) 72 | } 73 | 74 | // fetch accounts periodically 75 | pub fn start_watching( 76 | &self, 77 | pool_account_bin: Arc>>, 78 | bin: Arc>>, 79 | tx: Sender 80 | ) { 81 | let get_blocks = self.rpc_url.clone(); 82 | let rpc_client = RpcClient::new(get_blocks); 83 | let tx = tx.clone(); 84 | 85 | let items = Arc::clone(&pool_account_bin).lock().unwrap().iter().map(|account| { 86 | account.get_swap_related_pubkeys(Some(&rpc_client)).unwrap().into_iter().map(|item| { 87 | (account.market, item.0, item.1) 88 | }).collect::>() 89 | }).into_iter().flatten().collect::>(); 90 | 91 | spawn(async move { 92 | loop { 93 | Self::_fetch_multiple_accounts( 94 | &rpc_client, 95 | items.clone(), 96 | Arc::clone(&bin), 97 | Some(tx.clone()) 98 | ); 99 | 100 | let _ = sleep(Duration::from_secs(10)).await; 101 | } 102 | }); 103 | } 104 | 105 | fn _fetch_multiple_accounts( 106 | rpc_client: &RpcClient, 107 | items: Vec<(Market, DeserializedAccount, Pubkey)>, 108 | bin: Arc>>, 109 | tx: Option> 110 | ) { 111 | let time = Instant::now(); 112 | let pubkeys = items.iter().map(|item| { item.2 }).collect::>(); 113 | let accounts = Self::_fetch_accounts(&rpc_client, &pubkeys); 114 | 115 | let fetched_accounts = accounts.iter().enumerate().filter(|(index, account)| { 116 | account.is_some() 117 | }).map(|(index, account)| { 118 | let account = account.clone().unwrap(); 119 | 120 | match items[index].1 { 121 | DeserializedAccount::Account(_) => { 122 | DeserializedAccount::Account(DeserializedDataAccount { 123 | pubkey: items[index].2, 124 | account, 125 | market: items[index].0, 126 | }) 127 | } 128 | DeserializedAccount::PoolAccount(_) => { 129 | let market_operation = resolve_pool_account(&items[index].0, &account.data); 130 | DeserializedAccount::PoolAccount( 131 | DeserializedPoolAccount { 132 | pubkey: items[index].2, 133 | account, 134 | market: items[index].0, 135 | operation: market_operation, 136 | } 137 | ) 138 | } 139 | DeserializedAccount::TokenAccount(_) => { 140 | DeserializedAccount::TokenAccount(DeserializedTokenAccount { 141 | pubkey: pubkeys[index], 142 | account: account.clone(), 143 | token: TokenAccount::unpack_data(&account.data), 144 | market: items[index].0, 145 | }) 146 | } 147 | DeserializedAccount::ConfigAccount(_) => { 148 | DeserializedAccount::ConfigAccount( 149 | resolve_pool_config_account(&items[index].0, &account.owner, pubkeys[index], &account.data) 150 | ) 151 | } 152 | } 153 | }).collect::>(); 154 | 155 | // todo: replace not overwrite 156 | *bin.lock().unwrap() = fetched_accounts; 157 | if let Some(tx) = tx { 158 | tx.send(Event::UpdateAccounts).expect("broadcast: failed to broadcast Event::UpdateAccounts"); 159 | } 160 | 161 | println!("probe: accounts fetched ({:?})", time.elapsed()); 162 | } 163 | 164 | fn _fetch_accounts( 165 | rpc_client: &RpcClient, 166 | pubkeys: &Vec 167 | ) -> Vec> { 168 | let mut vec: Vec> = Vec::new(); 169 | 170 | pubkeys.chunks(99).for_each(|pubkeys| { 171 | match rpc_client.get_multiple_accounts(pubkeys) { 172 | Ok(mut accounts) => { 173 | vec.append(accounts.as_mut()) 174 | } 175 | Err(err) => { 176 | eprintln!("probe: failed to fetch pubkeys: {}", err); 177 | } 178 | } 179 | }); 180 | 181 | vec 182 | } 183 | } -------------------------------------------------------------------------------- /src/pubkey/meteora.json: -------------------------------------------------------------------------------- 1 | { 2 | "pools": [ 3 | "7qt1qBnQ5CNNpMH1no6jYAzuyazP5QWXsUZB7dot5kga", 4 | "HyhMt7jPKJ1LLXQTm5wjf5f4kWqAeTeKQZvMq8TtZnPV", 5 | "HTvjzsfX3yU6BUodCjZ5vZkUrAxMDTrBs3CJaq43ashR", 6 | "FbkX1h2YTs171cEMa4GrV7XbAiQt5zSmV2CjfYWxXJDP" 7 | ] 8 | } -------------------------------------------------------------------------------- /src/pubkey/orca.json: -------------------------------------------------------------------------------- 1 | { 2 | "pools": [ 3 | "C1MgLojNLWBKADvu9BHdtgzz1oZX4dZ5zGdGcgvvW8Wz", 4 | "4Ui9QdDNuUaAGqCPcDSp191QrixLzQiLxJ1Gnqvz3szP", 5 | "AyFajbj7QEi8CizFnfEjJn3vSUxgDjVKob4A8i618YJD", 6 | "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE", 7 | "D6NdKrKNQPmRZCCnG1GqXtF7MMoHB7qR6GU5TkG59Qz1", 8 | "AHTTzwf3GmVMJdxWM8v2MSxyjZj8rQR6hyAC3g9477Yj", 9 | "FpCMFDFGYotvufJ7HrFHsWEiiQCGbkLCtwHiDnh7o28Q", 10 | "ELrat5Lhdw7vqiXVg9erGtTFYNHNAJUdj3dBZgxaAdzr" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/pubkey/raydium.json: -------------------------------------------------------------------------------- 1 | { 2 | "pools": [ 3 | "8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj", 4 | "EZVkeboWeXygtq8LMyENHyXdF5wpYrtExRNH9UwB1qYw", 5 | "CYbD9RaToYMtWKA7QZyoLahnHdWq553Vm62Lh6qWtuxq", 6 | "22WrmyTj8x2TRVQen3fxxi2r4Rn6JDHWoMTpsSmn8RUd" 7 | ] 8 | } -------------------------------------------------------------------------------- /src/struct/account.rs: -------------------------------------------------------------------------------- 1 | use solana_client::rpc_client::RpcClient; 2 | use solana_sdk::account::Account; 3 | use solana_sdk::pubkey::Pubkey; 4 | use crate::formula::base::Formula; 5 | 6 | use crate::formula::clmm::constant::TICK_ARRAY_SEED; 7 | use crate::formula::clmm::orca_swap_state::{get_tick_array_public_keys_with_start_tick_index, TICK_ARRAY_SIZE, TickArray, TickArrayAccount}; 8 | use crate::formula::clmm::raydium_tick_array::{TickArrayBitmapExtension, TickArrayBitmapExtensionAccount, TickArrayState, TickArrayStateAccount}; 9 | use crate::r#struct::market::{Market, PoolOperation}; 10 | use crate::r#struct::pools::{OrcaClmmAccount, OrcaClmmMarket, RaydiumClmmAccount, RaydiumClmmMarket}; 11 | use crate::r#struct::resolver::resolve_pool_account; 12 | use crate::r#struct::token::TokenAccount; 13 | 14 | #[derive(Clone)] 15 | pub enum DeserializedAccount { 16 | Account(DeserializedDataAccount), 17 | PoolAccount(DeserializedPoolAccount), 18 | TokenAccount(DeserializedTokenAccount), 19 | ConfigAccount(DeserializedConfigAccount) 20 | } 21 | 22 | impl DeserializedAccount { 23 | pub fn get_pubkey(&self) -> Pubkey { 24 | match self { 25 | DeserializedAccount::Account(account) => { 26 | account.pubkey 27 | } 28 | DeserializedAccount::PoolAccount(account) => { 29 | account.pubkey 30 | } 31 | DeserializedAccount::TokenAccount(account) => { 32 | account.pubkey 33 | } 34 | DeserializedAccount::ConfigAccount(account) => { 35 | account.get_pubkey() 36 | } 37 | } 38 | } 39 | 40 | pub fn get_market(&self) -> Market { 41 | match self { 42 | DeserializedAccount::Account(account) => { 43 | account.market 44 | } 45 | DeserializedAccount::PoolAccount(account) => { 46 | account.market 47 | } 48 | DeserializedAccount::TokenAccount(account) => { 49 | account.market 50 | } 51 | DeserializedAccount::ConfigAccount(account) => { 52 | account.get_market() 53 | } 54 | } 55 | } 56 | } 57 | 58 | 59 | 60 | #[derive(Clone, Default, PartialEq)] 61 | pub enum DeserializedConfigAccount { 62 | RaydiumClmmConfigAccount(RaydiumClmmAccount), 63 | OrcaClmmConfigAccount(OrcaClmmAccount), 64 | #[default] 65 | EmptyConfigAccount 66 | } 67 | 68 | impl DeserializedConfigAccount { 69 | pub fn get_pubkey(&self) -> Pubkey { 70 | match self { 71 | DeserializedConfigAccount::RaydiumClmmConfigAccount(account) => { 72 | account.get_pubkey() 73 | } 74 | DeserializedConfigAccount::OrcaClmmConfigAccount(account) => { 75 | account.get_pubkey() 76 | } 77 | _ => { 78 | Pubkey::default() 79 | } 80 | } 81 | } 82 | 83 | pub fn get_market(&self) -> Market { 84 | match self { 85 | DeserializedConfigAccount::RaydiumClmmConfigAccount(account) => { 86 | account.get_market() 87 | } 88 | DeserializedConfigAccount::OrcaClmmConfigAccount(account) => { 89 | account.get_market() 90 | } 91 | _ => { 92 | Market::UNKNOWN 93 | } 94 | } 95 | } 96 | } 97 | 98 | #[derive(Clone, Default)] 99 | pub struct DeserializedPoolAccount { 100 | pub pubkey: Pubkey, 101 | pub account: Account, 102 | pub market: Market, 103 | pub operation: Box 104 | } 105 | 106 | impl DeserializedPoolAccount { 107 | pub fn get_swap_related_pubkeys(&self, rpc_client: Option<&RpcClient>) -> Result, &'static str> { 108 | match self.market { 109 | Market::ORCA => { 110 | let mut vec = vec![ 111 | (DeserializedAccount::PoolAccount(DeserializedPoolAccount::default()), self.pubkey) 112 | ]; 113 | vec.append(&mut self.operation.get_swap_related_pubkeys()); 114 | 115 | if self.operation.get_formula() == Formula::ConcentratedLiquidity { 116 | // let accounts = rpc_client.unwrap().get_multiple_accounts(&[self.pubkey, tick_array_bitmap_extension_pubkey]).expect("failed to fetch accounts"); 117 | let pool_account = rpc_client.unwrap().get_account(&self.pubkey).expect("failed to fetch pool"); 118 | let pool = resolve_pool_account(&Market::ORCA, &pool_account.data); 119 | let market = pool.as_any().downcast_ref::().expect("failed to downcast"); 120 | 121 | for i in 0..2 { 122 | let zero_for_one: bool = if i % 2 == 0 { true } else { false }; 123 | get_tick_array_public_keys_with_start_tick_index( 124 | market.tick_current_index, 125 | market.tick_spacing, 126 | zero_for_one, 127 | &self.account.owner, 128 | &self.pubkey, 129 | ).iter().for_each(|pubkey| { 130 | vec.push(( 131 | DeserializedAccount::ConfigAccount(DeserializedConfigAccount::OrcaClmmConfigAccount(OrcaClmmAccount::TickArray(TickArrayAccount::default()))), 132 | *pubkey 133 | )); 134 | }); 135 | } 136 | } 137 | 138 | Ok(vec) 139 | } 140 | Market::RAYDIUM => { 141 | let mut vec = vec![ 142 | (DeserializedAccount::PoolAccount(DeserializedPoolAccount::default()), self.pubkey) 143 | ]; 144 | vec.append(&mut self.operation.get_swap_related_pubkeys()); 145 | 146 | // since this step does not know swap direction, find both ways of tick array states pubkeys 147 | if self.operation.get_formula() == Formula::ConcentratedLiquidity { 148 | // get tick array states 149 | let tick_array_bitmap_extension_pubkey = TickArrayBitmapExtension::key(&self.account.owner, &self.pubkey).expect("failed to get tick_array_bitmap_extension pubkey"); 150 | let accounts = rpc_client.unwrap().get_multiple_accounts(&[self.pubkey, tick_array_bitmap_extension_pubkey]).expect("failed to fetch accounts"); 151 | 152 | let tick_array_bitmap_extension_account = accounts[1].to_owned().expect("failed to fetch tick_array_bitmap_extension"); 153 | let tick_array_bitmap_extension = TickArrayBitmapExtension::unpack_data(&tick_array_bitmap_extension_account.data); 154 | vec.push((DeserializedAccount::ConfigAccount(DeserializedConfigAccount::RaydiumClmmConfigAccount(RaydiumClmmAccount::TickArrayBitmapExtension(TickArrayBitmapExtensionAccount::default()))), tick_array_bitmap_extension_pubkey)); 155 | 156 | let pool_account = accounts[0].to_owned().expect("failed to fetch pool"); 157 | let pool = resolve_pool_account(&Market::RAYDIUM, &pool_account.data); 158 | let market = pool.as_any().downcast_ref::().expect("failed to downcast"); 159 | 160 | for i in 0..2 { 161 | let zero_for_one = if i % 2 == 0 { true } else { false }; 162 | 163 | let (_, mut current_valid_tick_array_start_index) = market.get_first_initialized_tick_array( 164 | &Some(&tick_array_bitmap_extension), true 165 | ).unwrap(); 166 | let current_tick_array_state = TickArrayState::key( 167 | &self.account.owner, 168 | &[ 169 | &TICK_ARRAY_SEED.as_bytes(), 170 | &self.pubkey.as_ref(), 171 | ¤t_valid_tick_array_start_index.to_be_bytes() 172 | ] 173 | ).expect("failed to get current_tick_array_state"); 174 | vec.push((DeserializedAccount::ConfigAccount(DeserializedConfigAccount::RaydiumClmmConfigAccount(RaydiumClmmAccount::TickArrayState(TickArrayStateAccount::default()))), current_tick_array_state)); 175 | 176 | for _ in 0..3 { 177 | let next_tick_array_index = market.next_initialized_tick_array_start_index( 178 | &Some(&tick_array_bitmap_extension), 179 | current_valid_tick_array_start_index, 180 | zero_for_one 181 | ).expect("failed to get next_tick_array_index"); 182 | 183 | if next_tick_array_index.is_none() { 184 | break; 185 | } 186 | current_valid_tick_array_start_index = next_tick_array_index.unwrap(); 187 | let tick_array_state = TickArrayState::key( 188 | &self.account.owner, 189 | &[ 190 | &TICK_ARRAY_SEED.as_bytes(), 191 | &self.pubkey.as_ref(), 192 | ¤t_valid_tick_array_start_index.to_be_bytes() 193 | ] 194 | ).expect("failed to get tick_array_state"); 195 | // todo tick_array_state array does not need to have 5 items 196 | vec.push((DeserializedAccount::ConfigAccount(DeserializedConfigAccount::RaydiumClmmConfigAccount(RaydiumClmmAccount::TickArrayState(TickArrayStateAccount::default()))), tick_array_state)); 197 | } 198 | } 199 | } 200 | 201 | Ok(vec) 202 | } 203 | Market::METEORA | Market::LIFINITY => { todo!() } 204 | Market::UNKNOWN => { Err("unknown market") } 205 | } 206 | } 207 | 208 | pub fn equals(&self, to: &DeserializedPoolAccount) -> bool { 209 | self.pubkey == to.pubkey 210 | } 211 | } 212 | 213 | #[derive(Clone, Default)] 214 | pub struct DeserializedDataAccount { 215 | pub pubkey: Pubkey, 216 | pub account: Account, 217 | pub market: Market, 218 | } 219 | 220 | #[derive(Clone, Default)] 221 | pub struct DeserializedTokenAccount { 222 | pub pubkey: Pubkey, 223 | pub account: Account, 224 | pub token: TokenAccount, 225 | pub market: Market, 226 | } 227 | 228 | impl DeserializedTokenAccount { 229 | pub fn get_amount(&self) -> u64 { 230 | self.token.amount 231 | } 232 | } 233 | 234 | pub trait AccountDataSerializer { 235 | fn unpack_data(data: &Vec) -> Self; 236 | } -------------------------------------------------------------------------------- /src/struct/market.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::fmt::{Debug, Display}; 3 | 4 | use dyn_clone::DynClone; 5 | use serde::de::DeserializeOwned; 6 | use serde::Deserialize; 7 | use solana_sdk::account::Account; 8 | use solana_sdk::pubkey::Pubkey; 9 | 10 | use crate::r#struct::account::{DeserializedAccount}; 11 | use crate::formula::base::Formula; 12 | use crate::utils::PubkeyPair; 13 | 14 | #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Default, Deserialize)] 15 | pub enum Market { 16 | ORCA, 17 | RAYDIUM, 18 | METEORA, 19 | LIFINITY, 20 | #[default] 21 | UNKNOWN 22 | } 23 | 24 | impl Market { 25 | pub fn from(market: &Market) -> Market { 26 | match market { 27 | Market::ORCA => Market::ORCA, 28 | Market::RAYDIUM => Market::RAYDIUM, 29 | Market::METEORA => Market::METEORA, 30 | Market::LIFINITY => Market::LIFINITY, 31 | Market::UNKNOWN => Market::UNKNOWN, 32 | } 33 | } 34 | } 35 | 36 | impl Market { 37 | pub fn name(&self) -> String { 38 | match self { 39 | Market::ORCA => String::from("ORCA"), 40 | Market::RAYDIUM => String::from("RAYDIUM"), 41 | Market::METEORA => String::from("METEORA"), 42 | Market::LIFINITY => String::from("LIFINITY"), 43 | Market::UNKNOWN => String::from("UNKNOWN"), 44 | } 45 | } 46 | 47 | 48 | } 49 | 50 | pub trait PoolOperation: DynClone + Sync + Send { 51 | fn get_mint_pair(&self) -> PubkeyPair; 52 | fn get_pool_pair(&self) -> PubkeyPair; 53 | fn get_swap_related_pubkeys(&self) -> Vec<(DeserializedAccount, Pubkey)>; 54 | fn get_formula(&self) -> Formula; 55 | fn swap(&self, accounts: &Vec); 56 | fn as_any(&self) -> &dyn Any; 57 | } 58 | 59 | impl Clone for Box { 60 | fn clone(&self) -> Self { 61 | dyn_clone::clone_box(&**self) 62 | } 63 | } 64 | 65 | impl Default for Box { 66 | fn default() -> Self { 67 | Box::new(::default()) 68 | } 69 | } 70 | 71 | pub trait AccountResolver { 72 | fn resolve_account(account: &Account) -> T; 73 | } 74 | 75 | #[derive(Copy, Clone, Default)] 76 | struct DefaultMarket {} 77 | 78 | impl PoolOperation for DefaultMarket { 79 | fn get_mint_pair(&self) -> PubkeyPair { 80 | PubkeyPair::default() 81 | } 82 | 83 | fn get_pool_pair(&self) -> PubkeyPair { 84 | PubkeyPair::default() 85 | } 86 | 87 | fn get_swap_related_pubkeys(&self) -> Vec<(DeserializedAccount, Pubkey)> { 88 | Vec::default() 89 | } 90 | 91 | fn get_formula(&self) -> Formula { 92 | Formula::default() 93 | } 94 | 95 | fn swap(&self, accounts: &Vec) {} 96 | 97 | fn as_any(&self) -> &dyn Any { 98 | self 99 | } 100 | } -------------------------------------------------------------------------------- /src/struct/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token; 2 | 3 | pub mod pools; 4 | pub mod market; 5 | pub mod account; 6 | pub mod resolver; 7 | -------------------------------------------------------------------------------- /src/struct/pools/lifinity.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use arrayref::{array_ref, array_refs}; 3 | use solana_sdk::pubkey::Pubkey; 4 | 5 | use crate::r#struct::account::{AccountDataSerializer, DeserializedAccount, DeserializedTokenAccount}; 6 | use crate::formula::base::Formula; 7 | use crate::formula::base::Formula::ConcentratedLiquidity; 8 | use crate::r#struct::market::PoolOperation; 9 | use crate::utils::PubkeyPair; 10 | 11 | #[derive(Copy, Clone, Debug, Default)] 12 | pub struct LifinityMarket { // 895 13 | pub initializer_key: Pubkey, // 32 14 | pub initializer_deposit_token_account: Pubkey, // 32 15 | pub initializer_receiver_token_account: Pubkey, // 32 16 | pub initializer_amount: u64, // 8 17 | pub taker_amount: u64, // 8 18 | pub is_initialized: bool, // 1 19 | pub bump_seed: u8, // 1 20 | pub freeze_trade: u8, // 1 21 | pub freeze_deposit: u8, // 1 22 | pub freeze_withdraw: u8, // 1 23 | pub base_decimals: u8, // 1 24 | pub token_program_id: Pubkey, // 32 25 | pub token_a_account: Pubkey, // 32 26 | pub token_b_account: Pubkey, // 32 27 | pub pool_mint: Pubkey, // 32 28 | pub token_a_mint: Pubkey, // 32 29 | pub token_b_mint: Pubkey, // 32 30 | pub fee_account: Pubkey, // 32 31 | pub oracle_main_account: Pubkey, // 32 32 | pub oracle_sub_account: Pubkey, // 32 33 | pub oracle_pc_account: Pubkey, // 32 34 | pub fees: AmmFees, // 64 35 | pub curve: AmmCurve, // 9 36 | pub config: AmmConfig, // 224 37 | pub amm_p_temp1: Pubkey, // 32 38 | pub amm_p_temp2: Pubkey, // 32 39 | pub amm_p_temp3: Pubkey, // 32 40 | pub amm_p_temp4: Pubkey, // 32 41 | pub amm_p_temp5: Pubkey, // 32 42 | } 43 | 44 | impl AccountDataSerializer for LifinityMarket { 45 | fn unpack_data(data: &Vec) -> Self { 46 | let src = array_ref![data, 0, 903]; 47 | let (discriminator, initializer_key, initializer_deposit_token_account, initializer_receiver_token_account, initializer_amount, taker_amount, is_initialized, bump_seed, freeze_trade, freeze_deposit, freeze_withdraw, base_decimals, token_program_id, token_a_account, token_b_account, pool_mint, token_a_mint, token_b_mint, fee_account, oracle_main_account, oracle_sub_account, oracle_pc_account, fees, curve, config, amm_p_temp1, amm_p_temp2, amm_p_temp3, amm_p_temp4, amm_p_temp5) = 48 | array_refs![src, 8, 32, 32, 32, 8, 8, 1, 1, 1, 1, 1, 1, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 64, 9, 224, 32, 32, 32, 32, 32]; 49 | 50 | LifinityMarket { 51 | initializer_key: Pubkey::new_from_array(*initializer_key), 52 | initializer_deposit_token_account: Pubkey::new_from_array(*initializer_deposit_token_account), 53 | initializer_receiver_token_account: Pubkey::new_from_array(*initializer_receiver_token_account), 54 | initializer_amount: u64::from_le_bytes(*initializer_amount), 55 | taker_amount: u64::from_le_bytes(*taker_amount), 56 | is_initialized: bool::from(true), 57 | bump_seed: u8::from_le_bytes(*bump_seed), 58 | freeze_trade: u8::from_le_bytes(*freeze_trade), 59 | freeze_deposit: u8::from_le_bytes(*freeze_deposit), 60 | freeze_withdraw: u8::from_le_bytes(*freeze_withdraw), 61 | base_decimals: u8::from_le_bytes(*base_decimals), 62 | token_program_id: Pubkey::new_from_array(*token_program_id), 63 | token_a_account: Pubkey::new_from_array(*token_a_account), 64 | token_b_account: Pubkey::new_from_array(*token_b_account), 65 | pool_mint: Pubkey::new_from_array(*pool_mint), 66 | token_a_mint: Pubkey::new_from_array(*token_a_mint), 67 | token_b_mint: Pubkey::new_from_array(*token_b_mint), 68 | fee_account: Pubkey::new_from_array(*fee_account), 69 | oracle_main_account: Pubkey::new_from_array(*oracle_main_account), 70 | oracle_sub_account: Pubkey::new_from_array(*oracle_sub_account), 71 | oracle_pc_account: Pubkey::new_from_array(*oracle_pc_account), 72 | fees: AmmFees::unpack_data(&Vec::from(fees)), 73 | curve: AmmCurve::unpack_data(*curve), 74 | config: AmmConfig::unpack_data(*config), 75 | amm_p_temp1: Pubkey::new_from_array(*amm_p_temp1), 76 | amm_p_temp2: Pubkey::new_from_array(*amm_p_temp2), 77 | amm_p_temp3: Pubkey::new_from_array(*amm_p_temp3), 78 | amm_p_temp4: Pubkey::new_from_array(*amm_p_temp4), 79 | amm_p_temp5: Pubkey::new_from_array(*amm_p_temp5), 80 | } 81 | } 82 | } 83 | 84 | impl PoolOperation for LifinityMarket { 85 | fn get_mint_pair(&self) -> PubkeyPair { 86 | PubkeyPair { 87 | pubkey_a: self.token_a_mint, 88 | pubkey_b: self.token_b_mint 89 | } 90 | } 91 | 92 | fn get_pool_pair(&self) -> PubkeyPair { 93 | PubkeyPair { 94 | pubkey_a: self.token_a_account, 95 | pubkey_b: self.token_b_account 96 | } 97 | } 98 | 99 | fn get_swap_related_pubkeys(&self) -> Vec<(DeserializedAccount, Pubkey)> { 100 | vec![ 101 | (DeserializedAccount::TokenAccount(DeserializedTokenAccount::default()), self.token_a_account), 102 | (DeserializedAccount::TokenAccount(DeserializedTokenAccount::default()), self.token_b_account), 103 | ] 104 | } 105 | 106 | fn get_formula(&self) -> Formula { 107 | ConcentratedLiquidity 108 | } 109 | 110 | fn swap(&self, accounts: &Vec) { 111 | todo!() 112 | } 113 | 114 | fn as_any(&self) -> &dyn Any { 115 | self 116 | } 117 | } 118 | 119 | #[derive(Copy, Clone, Debug, Default)] 120 | pub struct AmmFees { // 64 121 | pub trade_fee_numerator: u64, // 8 122 | pub trade_fee_denominator: u64, // 8 123 | pub owner_trade_fee_numerator: u64, // 8 124 | pub owner_trade_fee_denominator: u64, // 8 125 | pub owner_withdraw_fee_numerator: u64, // 8 126 | pub owner_withdraw_fee_denominator: u64, // 8 127 | pub host_fee_numerator: u64, // 8 128 | pub host_fee_denominator: u64, // 8 129 | } 130 | 131 | impl AccountDataSerializer for AmmFees { 132 | fn unpack_data(data: &Vec) -> Self { 133 | let src = array_ref![data, 0, 64]; 134 | let (trade_fee_numerator, trade_fee_denominator, owner_trade_fee_numerator, owner_trade_fee_denominator, owner_withdraw_fee_numerator, owner_withdraw_fee_denominator, host_fee_numerator, host_fee_denominator) = 135 | array_refs![src, 8, 8, 8, 8, 8, 8, 8, 8]; 136 | 137 | AmmFees { 138 | trade_fee_numerator: u64::from_le_bytes(*trade_fee_numerator), 139 | trade_fee_denominator: u64::from_le_bytes(*trade_fee_denominator), 140 | owner_trade_fee_numerator: u64::from_le_bytes(*owner_trade_fee_numerator), 141 | owner_trade_fee_denominator: u64::from_le_bytes(*owner_trade_fee_denominator), 142 | owner_withdraw_fee_numerator: u64::from_le_bytes(*owner_withdraw_fee_numerator), 143 | owner_withdraw_fee_denominator: u64::from_le_bytes(*owner_withdraw_fee_denominator), 144 | host_fee_numerator: u64::from_le_bytes(*host_fee_numerator), 145 | host_fee_denominator: u64::from_le_bytes(*host_fee_denominator), 146 | } 147 | } 148 | } 149 | 150 | #[derive(Copy, Clone, Debug, Default)] 151 | pub struct AmmCurve { // 9 152 | pub curve_type: u8, // 1 153 | pub curve_parameters: u64 // 8 154 | } 155 | 156 | impl AmmCurve { 157 | pub fn unpack_data(data: [u8; 9]) -> AmmCurve { 158 | let src = array_ref![data, 0, 9]; 159 | let (curve_type, curve_parameters) = 160 | array_refs![src, 1, 8]; 161 | 162 | AmmCurve { 163 | curve_type: u8::from_le_bytes(*curve_type), 164 | curve_parameters: u64::from_le_bytes(*curve_parameters), 165 | } 166 | } 167 | } 168 | 169 | #[derive(Copy, Clone, Debug, Default)] 170 | pub struct AmmConfig { // 224 171 | pub last_price: u64, // 8 172 | pub last_balance_price: u64, // 8 173 | pub config_denominator: u64, // 8 174 | pub volume_x: u64, // 8 175 | pub volume_y: u64, // 8 176 | pub volume_x_in_y: u64, // 8 177 | pub deposit_cap: u64, // 8 178 | pub regression_target: u64, // 8 179 | pub oracle_type: u64, // 8 180 | pub oracle_status: u64, // 8 181 | pub oracle_main_slot_limit: u64, // 8 182 | pub oracle_sub_confidence_limit: u64, // 8 183 | pub oracle_sub_slot_limit: u64, // 8 184 | pub oracle_pc_confidence_limit: u64, // 8 185 | pub std_spread: u64, // 8 186 | pub std_spread_buffer: u64, // 8 187 | pub spread_coefficient: u64, // 8 188 | pub price_buffer_coin: i64, // 8 189 | pub price_buffer_pc: i64, // 8 190 | pub rebalance_ratio: u64, // 8 191 | pub fee_trade: u64, // 8 192 | pub fee_platform: u64, // 8 193 | pub config_temp3: u64, // 8 194 | pub config_temp4: u64, // 8 195 | pub config_temp5: u64, // 8 196 | pub config_temp6: u64, // 8 197 | pub config_temp7: u64, // 8 198 | pub config_temp8: u64, // 8 199 | } 200 | 201 | impl AmmConfig { 202 | pub fn unpack_data(data: [u8; 224]) -> AmmConfig { 203 | let src = array_ref![data, 0, 224]; 204 | let (last_price, last_balance_price, config_denominator, volume_x, volume_y, volume_x_in_y, deposit_cap, regression_target, oracle_type, oracle_status, oracle_main_slot_limit, oracle_sub_confidence_limit, oracle_sub_slot_limit, oracle_pc_confidence_limit, std_spread, std_spread_buffer, spread_coefficient, price_buffer_coin, price_buffer_pc, rebalance_ratio, fee_trade, fee_platform, config_temp3, config_temp4, config_temp5, config_temp6, config_temp7, config_temp8) = 205 | array_refs![src, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8]; 206 | 207 | AmmConfig { 208 | last_price: u64::from_le_bytes(*last_price), 209 | last_balance_price: u64::from_le_bytes(*last_balance_price), 210 | config_denominator: u64::from_le_bytes(*config_denominator), 211 | volume_x: u64::from_le_bytes(*volume_x), 212 | volume_y: u64::from_le_bytes(*volume_y), 213 | volume_x_in_y: u64::from_le_bytes(*volume_x_in_y), 214 | deposit_cap: u64::from_le_bytes(*deposit_cap), 215 | regression_target: u64::from_le_bytes(*regression_target), 216 | oracle_type: u64::from_le_bytes(*oracle_type), 217 | oracle_status: u64::from_le_bytes(*oracle_status), 218 | oracle_main_slot_limit: u64::from_le_bytes(*oracle_main_slot_limit), 219 | oracle_sub_confidence_limit: u64::from_le_bytes(*oracle_sub_confidence_limit), 220 | oracle_sub_slot_limit: u64::from_le_bytes(*oracle_sub_slot_limit), 221 | oracle_pc_confidence_limit: u64::from_le_bytes(*oracle_pc_confidence_limit), 222 | std_spread: u64::from_le_bytes(*std_spread), 223 | std_spread_buffer: u64::from_le_bytes(*std_spread_buffer), 224 | spread_coefficient: u64::from_le_bytes(*spread_coefficient), 225 | price_buffer_coin: i64::from_le_bytes(*price_buffer_coin), 226 | price_buffer_pc: i64::from_le_bytes(*price_buffer_pc), 227 | rebalance_ratio: u64::from_le_bytes(*rebalance_ratio), 228 | fee_trade: u64::from_le_bytes(*fee_trade), 229 | fee_platform: u64::from_le_bytes(*fee_platform), 230 | config_temp3: u64::from_le_bytes(*config_temp3), 231 | config_temp4: u64::from_le_bytes(*config_temp4), 232 | config_temp5: u64::from_le_bytes(*config_temp5), 233 | config_temp6: u64::from_le_bytes(*config_temp6), 234 | config_temp7: u64::from_le_bytes(*config_temp7), 235 | config_temp8: u64::from_le_bytes(*config_temp8), 236 | } 237 | } 238 | } -------------------------------------------------------------------------------- /src/struct/pools/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod orca; 2 | pub mod meteora; 3 | pub mod raydium; 4 | pub mod lifinity; 5 | 6 | pub use orca::*; 7 | pub use meteora::*; 8 | pub use raydium::*; -------------------------------------------------------------------------------- /src/struct/resolver.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use solana_sdk::pubkey::Pubkey; 3 | 4 | use crate::r#struct::account::{AccountDataSerializer, DeserializedConfigAccount}; 5 | use crate::constants::RAYDIUM_CLMM_DATA_LEN; 6 | use crate::constants::RAYDIUM_CLMM_PROGRAM_PUBKEY; 7 | use crate::constants::RAYDIUM_OPEN_BOOK_PROGRAM_PUBKEY; 8 | use crate::r#struct::market::{Market, PoolOperation}; 9 | use crate::r#struct::pools::{MeteoraClmmMarket, OrcaClmmAccount, OrcaClmmMarket, RaydiumClmmAccount, RaydiumClmmMarket, RaydiumOpenBookMarket, WhirlpoolsConfig, WhirlpoolsConfigAccount}; 10 | use crate::r#struct::pools::lifinity::LifinityMarket; 11 | 12 | pub fn resolve_pool_account(market: &Market, data: &Vec) -> Box { 13 | match market { 14 | Market::ORCA => { 15 | Box::new(OrcaClmmMarket::unpack_data(data)) 16 | } 17 | Market::RAYDIUM => { 18 | if data.len() == RAYDIUM_CLMM_DATA_LEN { 19 | Box::new(RaydiumClmmMarket::unpack_data(data)) 20 | } 21 | else { 22 | Box::new(RaydiumOpenBookMarket::unpack_data(data)) 23 | } 24 | } 25 | Market::METEORA => { 26 | Box::new(MeteoraClmmMarket::unpack_data(data)) 27 | } 28 | Market::LIFINITY => { 29 | Box::new(LifinityMarket::unpack_data(data)) 30 | } 31 | _ => { 32 | panic!("unknown pool") 33 | } 34 | } 35 | } 36 | 37 | pub fn resolve_pool_config_account(market: &Market, owner_pubkey: &Pubkey, account_pubkey: Pubkey, data: &Vec) -> DeserializedConfigAccount { 38 | match market { 39 | Market::ORCA => { 40 | DeserializedConfigAccount::OrcaClmmConfigAccount( 41 | OrcaClmmAccount::resolve_account(account_pubkey, data) 42 | ) 43 | } 44 | Market::RAYDIUM => { 45 | match owner_pubkey.to_string().as_str() { 46 | RAYDIUM_CLMM_PROGRAM_PUBKEY => { 47 | DeserializedConfigAccount::RaydiumClmmConfigAccount( 48 | RaydiumClmmAccount::resolve_account(account_pubkey, data) 49 | ) 50 | } 51 | RAYDIUM_OPEN_BOOK_PROGRAM_PUBKEY => { 52 | panic!("unknown account: RaydiumOpenBookAccount") 53 | } 54 | _ => { 55 | DeserializedConfigAccount::EmptyConfigAccount 56 | } 57 | } 58 | } 59 | Market::METEORA => { 60 | todo!() 61 | } 62 | Market::LIFINITY => { 63 | todo!() 64 | } 65 | _ => { 66 | todo!() 67 | } 68 | } 69 | } 70 | 71 | pub fn resolve_token_data() {} -------------------------------------------------------------------------------- /src/struct/token.rs: -------------------------------------------------------------------------------- 1 | use arrayref::{array_ref, array_refs}; 2 | use num_enum::{TryFromPrimitive}; 3 | use solana_sdk::program_error::ProgramError; 4 | use solana_sdk::program_option::COption; 5 | use solana_sdk::pubkey::Pubkey; 6 | use crate::r#struct::account::AccountDataSerializer; 7 | 8 | #[repr(u8)] 9 | #[derive(Clone, Default, Eq, PartialEq, TryFromPrimitive)] 10 | pub enum AccountState { 11 | #[default] 12 | Uninitialized = 0, 13 | Initialized = 1, 14 | Frozen = 2, 15 | } 16 | 17 | #[derive(Clone, Default)] 18 | pub struct TokenAccount { 19 | pub mint: Pubkey, 20 | pub owner: Pubkey, 21 | pub amount: u64, 22 | pub delegate: COption, 23 | pub state: AccountState, 24 | pub is_native: COption, 25 | pub delegated_amount: u64, 26 | pub close_authority: COption 27 | } 28 | 29 | impl AccountDataSerializer for TokenAccount { 30 | fn unpack_data(data: &Vec) -> TokenAccount { 31 | let src = array_ref![data, 0, 165]; 32 | let (mint, owner, amount, delegate, state, is_native, delegated_amount, close_authority) = 33 | array_refs![src, 32, 32, 8, 36, 1, 12, 8, 36]; 34 | 35 | TokenAccount { 36 | mint: Pubkey::new_from_array(*mint), 37 | owner: Pubkey::new_from_array(*owner), 38 | amount: u64::from_le_bytes(*amount), 39 | delegate: Self::unpack_coption_key(delegate).unwrap(), 40 | state: AccountState::try_from_primitive(u8::from_le_bytes(*state)).or(Err(ProgramError::InvalidAccountData)).unwrap(), 41 | is_native: Self::unpack_coption_u64(is_native).unwrap(), 42 | delegated_amount: u64::from_le_bytes(*delegated_amount), 43 | close_authority: Self::unpack_coption_key(close_authority).unwrap(), 44 | } 45 | } 46 | } 47 | 48 | impl TokenAccount { 49 | fn unpack_coption_key(src: &[u8; 36]) -> Result, ProgramError> { 50 | let (tag, body) = array_refs![src, 4, 32]; 51 | match *tag { 52 | [0, 0, 0, 0] => Ok(COption::None), 53 | [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))), 54 | _ => Err(ProgramError::InvalidAccountData), 55 | } 56 | } 57 | 58 | fn unpack_coption_u64(src: &[u8; 12]) -> Result, ProgramError> { 59 | let (tag, body) = array_refs![src, 4, 8]; 60 | match *tag { 61 | [0, 0, 0, 0] => Ok(COption::None), 62 | [1, 0, 0, 0] => Ok(COption::Some(u64::from_le_bytes(*body))), 63 | _ => Err(ProgramError::InvalidAccountData), 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/temp.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | pub fn path() { 5 | let max_depth = 4usize; 6 | let arr: Rc)>>> = Rc::new(RefCell::new(Vec::from([ 7 | (0, vec![(0, 1), (1, 2), (2, 1), (3, 1), (4, 2), (1, 2), (1, 2)]), 8 | (1, vec![(0, 1), (1, 2), (4, 2), (4, 1), (3, 1), (3, 2)]) 9 | ]))); 10 | let visited: Rc>> = Rc::new(RefCell::new(Vec::new())); 11 | 12 | let len = Rc::clone(&arr).borrow().clone().len(); 13 | for i in 2..len+1 { 14 | base2(&max_depth, Rc::clone(&arr), Rc::clone(&visited), 0, i, 1, 1); 15 | } 16 | } 17 | 18 | pub fn base2(max_depth: &usize, arr: Rc)>>>, visited: Rc>>, start: usize, r: usize, target_mint: i32, round_trip_mint: i32) { 19 | if r == 0 { 20 | if validate_path(max_depth, &*Rc::clone(&visited).borrow(), &round_trip_mint) { 21 | println!("{}", 22 | Rc::clone(&visited).borrow().iter().map(|x1| {format!("market: {}, [{}, {}]", x1.0.to_string(), x1.1.0, x1.1.1)}).collect::>().join(",") 23 | ); 24 | } 25 | return; 26 | } 27 | else { 28 | for i in start..Rc::clone(&arr).borrow().len() { 29 | let d = Rc::clone(&arr).borrow()[i].clone(); 30 | let filtered = d.1.iter().filter(|x| { 31 | x.0 == target_mint || x.1 == target_mint 32 | }); 33 | println!("{}", filtered.clone().collect::>().len()); 34 | filtered.for_each(|x2| { 35 | 36 | let pair = x2; 37 | let mut target: (i32, i32) = (*pair).clone(); 38 | 39 | if target.0 != target_mint { 40 | target = (target_mint, target.0); 41 | } 42 | 43 | Rc::clone(&visited).borrow_mut().push((d.0, target)); 44 | 45 | let new_target_mint = if pair.0 == target_mint { 46 | pair.1 47 | } 48 | else { 49 | pair.0 50 | }; 51 | 52 | base2(max_depth, Rc::clone(&arr), Rc::clone(&visited), i+1, r-1, new_target_mint, round_trip_mint); 53 | Rc::clone(&visited).borrow_mut().pop(); 54 | }); 55 | } 56 | } 57 | } 58 | 59 | pub fn validate_path(max_depth: &usize, path: &Vec<(i32, (i32, i32))>, round_trip_mint: &i32) -> bool { 60 | if max_depth < &path.len() { 61 | false 62 | } 63 | else { 64 | if path.iter().filter(|sub_path| { 65 | sub_path.1.0 == *round_trip_mint || sub_path.1.1 == *round_trip_mint 66 | }).collect::>().len() == 2 { 67 | true 68 | } 69 | else { 70 | false 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::error::Error; 3 | use std::fs::File; 4 | use std::io::BufReader; 5 | use std::path::Path; 6 | use std::str::FromStr; 7 | use std::sync::{Arc, Mutex}; 8 | use num_enum::TryFromPrimitive; 9 | use num_integer::Integer; 10 | use serde::Deserialize; 11 | use serde_json::Value::Array; 12 | use solana_sdk::pubkey::Pubkey; 13 | use crate::r#struct::market::Market; 14 | use crate::r#struct::pools::RaydiumRewardInfo; 15 | 16 | #[derive(Copy, Clone, Debug, Default)] 17 | pub struct PubkeyPair { 18 | pub pubkey_a: Pubkey, 19 | pub pubkey_b: Pubkey 20 | } 21 | 22 | impl PubkeyPair { 23 | pub fn any(&self, pubkey: Pubkey) -> bool { 24 | self.pubkey_a == pubkey || self.pubkey_b == pubkey 25 | } 26 | 27 | pub fn all(&self, pubkey_a: Pubkey, pubkey_b: Pubkey) -> bool { 28 | (self.pubkey_a == pubkey_a && self.pubkey_b == pubkey_b) || (self.pubkey_a == pubkey_b && self.pubkey_b == pubkey_a) 29 | } 30 | 31 | pub fn none(&self, pubkey_a: Pubkey, pubkey_b: Pubkey) -> bool { 32 | (self.pubkey_a != pubkey_a && self.pubkey_b != pubkey_b) || (self.pubkey_a != pubkey_b && self.pubkey_b != pubkey_a) 33 | } 34 | } 35 | 36 | pub fn read_pools>(path: P) -> Result, Box> { 37 | let file = File::open(path).unwrap(); 38 | let buffer_reader = BufReader::new(file); 39 | 40 | let data: Pools = serde_json::from_reader(buffer_reader).unwrap(); 41 | let pools = data.pools.iter().map(|pool| {Pubkey::from_str(pool).unwrap()}).collect::>(); 42 | 43 | Ok(pools) 44 | } 45 | 46 | pub fn is_pool_account_pubkey(pools: Arc>>>, pubkey: &Pubkey) -> bool { 47 | pools.lock().unwrap().iter().any(|pool| { 48 | pool.1.iter().any(|pool_pubkey| { *pool_pubkey == *pubkey }) 49 | }) 50 | } 51 | 52 | #[derive(Deserialize, Debug)] 53 | pub struct Pools { 54 | pub pools: Vec 55 | } --------------------------------------------------------------------------------