├── .gitignore ├── src ├── vendor │ ├── mod.rs │ ├── solend │ │ ├── math │ │ │ ├── mod.rs │ │ │ ├── common.rs │ │ │ ├── rate.rs │ │ │ └── decimal.rs │ │ ├── mod.rs │ │ ├── state │ │ │ ├── last_update.rs │ │ │ ├── mod.rs │ │ │ ├── lending_market_metadata.rs │ │ │ ├── lending_market.rs │ │ │ └── rate_limiter.rs │ │ └── error.rs │ ├── kamino │ │ ├── last_update.rs │ │ ├── mod.rs │ │ ├── token_info.rs │ │ ├── obligation.rs │ │ ├── borrow_rate_curve.rs │ │ ├── reserve.rs │ │ └── fraction.rs │ ├── drift │ │ ├── mod.rs │ │ ├── user.rs │ │ └── spot_market.rs │ └── marginfi_v2.rs ├── amount.rs ├── field_as_string.rs ├── notifier.rs ├── get_transaction_balance_change.rs ├── helius_rpc.rs ├── metrics.rs ├── rpc_client_utils.rs ├── coinbase_exchange.rs ├── exchange.rs ├── lib.rs ├── coin_gecko.rs ├── priority_fee.rs ├── kraken_exchange.rs ├── binance_exchange.rs └── token.rs ├── LICENSE ├── fetch-release.sh ├── update-solana-dependencies.sh ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── patch.crates-io.sh ├── Cargo.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | *.db 3 | /sys 4 | .DS_Store 5 | /sell-your-sol/ 6 | -------------------------------------------------------------------------------- /src/vendor/mod.rs: -------------------------------------------------------------------------------- 1 | /// These projects don't provide a usable Rust SDK.. 2 | pub mod drift; 3 | pub mod kamino; 4 | pub mod marginfi_v2; 5 | pub mod solend; 6 | -------------------------------------------------------------------------------- /src/vendor/solend/math/mod.rs: -------------------------------------------------------------------------------- 1 | //! Math for preserving precision 2 | 3 | mod common; 4 | mod decimal; 5 | mod rate; 6 | 7 | pub use common::*; 8 | pub use decimal::*; 9 | pub use rate::*; 10 | -------------------------------------------------------------------------------- /src/vendor/kamino/last_update.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy)] 2 | #[repr(C)] 3 | pub struct LastUpdate { 4 | slot: u64, 5 | stale: u8, 6 | price_status: u8, 7 | placeholder: [u8; 6], 8 | } 9 | -------------------------------------------------------------------------------- /src/vendor/kamino/mod.rs: -------------------------------------------------------------------------------- 1 | /// Kamino bits yanked from https://github.com/Kamino-Finance/klend/blob/master/programs/klend/src 2 | mod borrow_rate_curve; 3 | mod fraction; 4 | mod last_update; 5 | mod obligation; 6 | mod reserve; 7 | mod token_info; 8 | 9 | pub use obligation::Obligation; 10 | pub use reserve::Reserve; 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /src/amount.rs: -------------------------------------------------------------------------------- 1 | pub enum Amount { 2 | Half, 3 | All, 4 | Exact(u64), 5 | } 6 | 7 | impl Amount { 8 | pub fn unwrap_or(self, all_amount: u64) -> u64 { 9 | match self { 10 | Self::All => all_amount, 11 | Self::Half => all_amount / 2, 12 | Self::Exact(exact) => exact, 13 | } 14 | } 15 | 16 | pub fn unwrap_or_else(self, f: F) -> u64 17 | where 18 | F: std::ops::FnOnce() -> u64, 19 | { 20 | if let Self::Exact(exact) = self { 21 | exact 22 | } else { 23 | f() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/field_as_string.rs: -------------------------------------------------------------------------------- 1 | use { 2 | serde::{de, Deserializer, Serializer}, 3 | serde::{Deserialize, Serialize}, 4 | std::str::FromStr, 5 | }; 6 | 7 | pub fn serialize(t: &T, serializer: S) -> Result 8 | where 9 | T: ToString, 10 | S: Serializer, 11 | { 12 | t.to_string().serialize(serializer) 13 | } 14 | 15 | pub fn deserialize<'de, T, D>(deserializer: D) -> Result 16 | where 17 | T: FromStr, 18 | D: Deserializer<'de>, 19 | ::Err: std::fmt::Debug, 20 | { 21 | let s: String = String::deserialize(deserializer)?; 22 | s.parse() 23 | .map_err(|e| de::Error::custom(format!("Parse error: {e:?}"))) 24 | } 25 | -------------------------------------------------------------------------------- /src/notifier.rs: -------------------------------------------------------------------------------- 1 | use {reqwest::Client, serde_json::json, std::env}; 2 | 3 | pub struct Notifier { 4 | client: Client, 5 | slack_webhook: Option, 6 | } 7 | 8 | impl Default for Notifier { 9 | fn default() -> Self { 10 | let slack_webhook = env::var("SLACK_WEBHOOK").ok(); 11 | Notifier { 12 | client: Client::new(), 13 | slack_webhook, 14 | } 15 | } 16 | } 17 | 18 | impl Notifier { 19 | pub async fn send(&self, msg: &str) { 20 | if let Some(ref slack_webhook) = self.slack_webhook { 21 | let data = json!({ "text": msg }); 22 | 23 | if let Err(err) = self.client.post(slack_webhook).json(&data).send().await { 24 | eprintln!("Failed to send Slack message: {err:?}"); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /fetch-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | BINS=( 6 | sys 7 | sys-tulip-deposit 8 | ) 9 | REPO=https://github.com/mvines/sys 10 | DEFAULT_TO_MASTER=1 11 | 12 | case "$(uname)" in 13 | Linux) 14 | TARGET=x86_64-unknown-linux-gnu 15 | ;; 16 | Darwin) 17 | TARGET=x86_64-apple-darwin 18 | ;; 19 | *) 20 | echo "machine architecture is currently unsupported" 21 | exit 1 22 | ;; 23 | esac 24 | 25 | for BIN in ${BINS[@]}; do 26 | BIN_TARGET=$BIN-$TARGET 27 | 28 | if [[ ( -z $1 && -n $DEFAULT_TO_MASTER ) || $1 = master ]]; then 29 | URL=$REPO/raw/master-bin/$BIN_TARGET 30 | elif [[ -n $1 ]]; then 31 | URL=$REPO/releases/download/$1/$BIN_TARGET 32 | else 33 | URL=$REPO/releases/latest/download/$BIN_TARGET 34 | fi 35 | 36 | ( 37 | set -x 38 | curl -fL $URL -o $BIN 39 | chmod +x $BIN 40 | ls -l $BIN 41 | ./$BIN --version 42 | ) 43 | done 44 | -------------------------------------------------------------------------------- /update-solana-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Updates the solana version in all the workspace crates 4 | # 5 | 6 | solana_ver=$1 7 | if [[ -z $solana_ver ]]; then 8 | echo "Usage: $0 " 9 | exit 1 10 | fi 11 | 12 | set -e 13 | 14 | cd "$(dirname "$0")" 15 | 16 | declare tomls=() 17 | while IFS='' read -r line; do tomls+=("$line"); done < <(find . -name Cargo.toml) 18 | 19 | crates=( 20 | solana-account-decoder 21 | solana-banks-client 22 | solana-banks-server 23 | solana-bpf-loader-program 24 | solana-clap-utils 25 | solana-cli-config 26 | solana-cli-output 27 | solana-client 28 | solana-core 29 | solana-logger 30 | solana-notifier 31 | solana-program 32 | solana-program-test 33 | solana-remote-wallet 34 | solana-runtime 35 | solana-sdk 36 | solana-stake-program 37 | solana-transaction-status 38 | solana-vote-program 39 | ) 40 | 41 | set -x 42 | for crate in "${crates[@]}"; do 43 | sed -i -e "s#\(${crate} = \"\)\(=\?\).*\(\"\)#\1\2$solana_ver\3#g" "${tomls[@]}" 44 | done 45 | -------------------------------------------------------------------------------- /src/vendor/solend/mod.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | //! A lending program for the Solana blockchain. 4 | 5 | pub mod error; 6 | //pub mod instruction; 7 | pub mod math; 8 | //pub mod oracles; 9 | pub mod state; 10 | 11 | /// mainnet program id 12 | pub mod solend_mainnet { 13 | solana_pubkey::declare_id!("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"); 14 | } 15 | 16 | /// devnet program id 17 | pub mod solend_devnet { 18 | solana_pubkey::declare_id!("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"); 19 | } 20 | 21 | /// Canonical null pubkey. Prints out as "nu11111111111111111111111111111111111111111" 22 | pub const NULL_PUBKEY: solana_pubkey::Pubkey = solana_pubkey::Pubkey::new_from_array([ 23 | 11, 193, 238, 216, 208, 116, 241, 195, 55, 212, 76, 22, 75, 202, 40, 216, 76, 206, 27, 169, 24 | 138, 64, 177, 28, 19, 90, 156, 0, 0, 0, 0, 0, 25 | ]); 26 | 27 | /// Mainnet program id for Switchboard v2. 28 | pub mod switchboard_v2_mainnet { 29 | solana_pubkey::declare_id!("SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"); 30 | } 31 | 32 | /// Devnet program id for Switchboard v2. 33 | pub mod switchboard_v2_devnet { 34 | solana_pubkey::declare_id!("2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG"); 35 | } 36 | -------------------------------------------------------------------------------- /src/vendor/kamino/token_info.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey::Pubkey; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | #[repr(C)] 5 | pub struct TokenInfo { 6 | pub name: [u8; 32], 7 | 8 | pub heuristic: PriceHeuristic, 9 | 10 | pub max_twap_divergence_bps: u64, 11 | 12 | pub max_age_price_seconds: u64, 13 | pub max_age_twap_seconds: u64, 14 | 15 | pub scope_configuration: ScopeConfiguration, 16 | 17 | pub switchboard_configuration: SwitchboardConfiguration, 18 | 19 | pub pyth_configuration: PythConfiguration, 20 | 21 | pub _padding: [u64; 20], 22 | } 23 | 24 | #[derive(Debug, Clone, Copy)] 25 | #[repr(C)] 26 | pub struct PriceHeuristic { 27 | pub lower: u64, 28 | pub upper: u64, 29 | pub exp: u64, 30 | } 31 | 32 | #[derive(Debug, Clone, Copy)] 33 | #[repr(C)] 34 | pub struct ScopeConfiguration { 35 | pub price_feed: Pubkey, 36 | pub price_chain: [u16; 4], 37 | pub twap_chain: [u16; 4], 38 | } 39 | 40 | #[derive(Debug, Clone, Copy)] 41 | #[repr(C)] 42 | pub struct SwitchboardConfiguration { 43 | pub price_aggregator: Pubkey, 44 | pub twap_aggregator: Pubkey, 45 | } 46 | 47 | #[derive(Debug, Clone, Copy)] 48 | #[repr(C)] 49 | pub struct PythConfiguration { 50 | pub price: Pubkey, 51 | } 52 | -------------------------------------------------------------------------------- /src/vendor/solend/math/common.rs: -------------------------------------------------------------------------------- 1 | //! Common module for Decimal and Rate 2 | 3 | use solana_program::program_error::ProgramError; 4 | 5 | /// Scale of precision 6 | pub const SCALE: usize = 18; 7 | /// Identity 8 | pub const WAD: u64 = 1_000_000_000_000_000_000; 9 | /// Half of identity 10 | pub const HALF_WAD: u64 = 500_000_000_000_000_000; 11 | /// Scale for percentages 12 | pub const PERCENT_SCALER: u64 = 10_000_000_000_000_000; 13 | /// Scale for basis points 14 | pub const BPS_SCALER: u64 = 100_000_000_000_000; 15 | 16 | /// Try to subtract, return an error on underflow 17 | pub trait TrySub: Sized { 18 | /// Subtract 19 | fn try_sub(self, rhs: Self) -> Result; 20 | } 21 | 22 | /// Try to subtract, return an error on overflow 23 | pub trait TryAdd: Sized { 24 | /// Add 25 | fn try_add(self, rhs: Self) -> Result; 26 | } 27 | 28 | /// Try to divide, return an error on overflow or divide by zero 29 | pub trait TryDiv: Sized { 30 | /// Divide 31 | fn try_div(self, rhs: RHS) -> Result; 32 | } 33 | 34 | /// Try to multiply, return an error on overflow 35 | pub trait TryMul: Sized { 36 | /// Multiply 37 | fn try_mul(self, rhs: RHS) -> Result; 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ master ] 4 | pull_request: 5 | branches: [ master ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-22.04 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: kenchan0130/actions-system-info@master 17 | id: system-info 18 | 19 | - if: runner.os == 'Linux' 20 | run: | 21 | sudo apt-get update 22 | sudo apt-get install -y libudev-dev 23 | 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: stable 28 | components: rustfmt, clippy 29 | 30 | - uses: actions/cache@v4 31 | with: 32 | path: | 33 | ~/.cargo/registry 34 | ~/.cargo/git 35 | target 36 | key: v1-${{ runner.os }}-${{ steps.system-info.outputs.cpu-model }}-${{ hashFiles('**/Cargo.lock') }} 37 | 38 | - name: cargo fmt 39 | uses: actions-rs/cargo@v1 40 | with: 41 | command: fmt 42 | args: --all -- --check 43 | 44 | - name: cargo clippy 45 | uses: actions-rs/cargo@v1 46 | with: 47 | command: clippy 48 | args: --workspace --all-targets -- --deny=warnings 49 | 50 | - name: cargo test 51 | uses: actions-rs/cargo@v1 52 | with: 53 | command: test 54 | args: --verbose 55 | 56 | - name: cargo build 57 | uses: actions-rs/cargo@v1 58 | with: 59 | command: build 60 | 61 | -------------------------------------------------------------------------------- /src/vendor/solend/state/last_update.rs: -------------------------------------------------------------------------------- 1 | use crate::vendor::solend::error::LendingError; 2 | use solana_program::{clock::Slot, program_error::ProgramError}; 3 | use std::cmp::Ordering; 4 | 5 | /// Number of slots to consider stale after 6 | pub const STALE_AFTER_SLOTS_ELAPSED: u64 = 1; 7 | 8 | /// Last update state 9 | #[derive(Clone, Debug, Default)] 10 | pub struct LastUpdate { 11 | /// Last slot when updated 12 | pub slot: Slot, 13 | /// True when marked stale, false when slot updated 14 | pub stale: bool, 15 | } 16 | 17 | impl LastUpdate { 18 | /// Create new last update 19 | pub fn new(slot: Slot) -> Self { 20 | Self { slot, stale: true } 21 | } 22 | 23 | /// Return slots elapsed since given slot 24 | pub fn slots_elapsed(&self, slot: Slot) -> Result { 25 | let slots_elapsed = slot 26 | .checked_sub(self.slot) 27 | .ok_or(LendingError::MathOverflow)?; 28 | Ok(slots_elapsed) 29 | } 30 | 31 | /// Set last update slot 32 | pub fn update_slot(&mut self, slot: Slot) { 33 | self.slot = slot; 34 | self.stale = false; 35 | } 36 | 37 | /// Set stale to true 38 | pub fn mark_stale(&mut self) { 39 | self.stale = true; 40 | } 41 | 42 | /// Check if marked stale or last update slot is too long ago 43 | pub fn is_stale(&self, slot: Slot) -> Result { 44 | Ok(self.stale || self.slots_elapsed(slot)? >= STALE_AFTER_SLOTS_ELAPSED) 45 | } 46 | } 47 | 48 | impl PartialEq for LastUpdate { 49 | fn eq(&self, other: &Self) -> bool { 50 | self.slot == other.slot 51 | } 52 | } 53 | 54 | impl PartialOrd for LastUpdate { 55 | fn partial_cmp(&self, other: &Self) -> Option { 56 | self.slot.partial_cmp(&other.slot) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/vendor/kamino/obligation.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{last_update::LastUpdate, reserve::BigFractionBytes}, 3 | solana_sdk::pubkey::Pubkey, 4 | }; 5 | 6 | static_assertions::const_assert_eq!(3336, std::mem::size_of::()); 7 | static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); 8 | #[derive(Debug, Clone, Copy)] 9 | #[repr(C, packed(1))] 10 | pub struct Obligation { 11 | pub tag: u64, 12 | pub last_update: LastUpdate, 13 | pub lending_market: Pubkey, 14 | pub owner: Pubkey, 15 | pub deposits: [ObligationCollateral; 8], 16 | pub lowest_reserve_deposit_ltv: u64, 17 | pub deposited_value_sf: u128, 18 | 19 | pub borrows: [ObligationLiquidity; 5], 20 | pub borrow_factor_adjusted_debt_value_sf: u128, 21 | pub borrowed_assets_market_value_sf: u128, 22 | pub allowed_borrow_value_sf: u128, 23 | pub unhealthy_borrow_value_sf: u128, 24 | 25 | pub deposits_asset_tiers: [u8; 8], 26 | pub borrows_asset_tiers: [u8; 5], 27 | 28 | pub elevation_group: u8, 29 | 30 | pub num_of_obsolete_reserves: u8, 31 | 32 | pub has_debt: u8, 33 | 34 | pub referrer: Pubkey, 35 | 36 | pub padding_3: [u64; 128], 37 | } 38 | 39 | #[derive(Debug, Clone, Copy)] 40 | #[repr(C, packed(1))] 41 | pub struct ObligationCollateral { 42 | pub deposit_reserve: Pubkey, 43 | pub deposited_amount: u64, 44 | pub market_value_sf: u128, 45 | pub padding: [u64; 10], 46 | } 47 | 48 | #[derive(Debug, Clone, Copy)] 49 | #[repr(C, packed(1))] 50 | pub struct ObligationLiquidity { 51 | pub borrow_reserve: Pubkey, 52 | pub cumulative_borrow_rate_bsf: BigFractionBytes, 53 | pub padding: u64, 54 | pub borrowed_amount_sf: u128, 55 | pub market_value_sf: u128, 56 | pub borrow_factor_adjusted_market_value_sf: u128, 57 | 58 | pub padding2: [u64; 8], 59 | } 60 | -------------------------------------------------------------------------------- /src/vendor/solend/state/mod.rs: -------------------------------------------------------------------------------- 1 | //! State types 2 | 3 | mod last_update; 4 | mod lending_market; 5 | mod lending_market_metadata; 6 | mod obligation; 7 | mod rate_limiter; 8 | mod reserve; 9 | 10 | pub use last_update::*; 11 | pub use lending_market::*; 12 | pub use lending_market_metadata::*; 13 | pub use obligation::*; 14 | pub use rate_limiter::*; 15 | pub use reserve::*; 16 | 17 | use crate::vendor::solend::math::{Decimal, WAD}; 18 | use solana_program::{msg, program_error::ProgramError}; 19 | 20 | /// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity) 21 | // @FIXME: restore to 5 22 | pub const INITIAL_COLLATERAL_RATIO: u64 = 1; 23 | const INITIAL_COLLATERAL_RATE: u64 = INITIAL_COLLATERAL_RATIO * WAD; 24 | 25 | /// Current version of the program and all new accounts created 26 | pub const PROGRAM_VERSION: u8 = 1; 27 | 28 | /// Accounts are created with data zeroed out, so uninitialized state instances 29 | /// will have the version set to 0. 30 | pub const UNINITIALIZED_VERSION: u8 = 0; 31 | 32 | /// Number of slots per year 33 | // 2 (slots per second) * 60 * 60 * 24 * 365 = 63072000 34 | pub const SLOTS_PER_YEAR: u64 = 63072000; 35 | 36 | // Helpers 37 | fn pack_decimal(decimal: Decimal, dst: &mut [u8; 16]) { 38 | *dst = decimal 39 | .to_scaled_val() 40 | .expect("Decimal cannot be packed") 41 | .to_le_bytes(); 42 | } 43 | 44 | fn unpack_decimal(src: &[u8; 16]) -> Decimal { 45 | Decimal::from_scaled_val(u128::from_le_bytes(*src)) 46 | } 47 | 48 | fn pack_bool(boolean: bool, dst: &mut [u8; 1]) { 49 | *dst = (boolean as u8).to_le_bytes() 50 | } 51 | 52 | fn unpack_bool(src: &[u8; 1]) -> Result { 53 | match u8::from_le_bytes(*src) { 54 | 0 => Ok(false), 55 | 1 => Ok(true), 56 | _ => { 57 | msg!("Boolean cannot be unpacked"); 58 | Err(ProgramError::InvalidAccountData) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/vendor/solend/state/lending_market_metadata.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use crate::vendor::solend::error::LendingError; 4 | use bytemuck::checked::try_from_bytes; 5 | use bytemuck::{Pod, Zeroable}; 6 | use solana_program::program_error::ProgramError; 7 | use solana_program::pubkey::Pubkey; 8 | use static_assertions::{assert_eq_size, const_assert}; 9 | 10 | /// market name size 11 | pub const MARKET_NAME_SIZE: usize = 50; 12 | 13 | /// market description size 14 | pub const MARKET_DESCRIPTION_SIZE: usize = 300; 15 | 16 | /// market image url size 17 | pub const MARKET_IMAGE_URL_SIZE: usize = 250; 18 | 19 | /// padding size 20 | pub const PADDING_SIZE: usize = 100; 21 | 22 | /// Lending market state 23 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 24 | #[repr(C)] 25 | pub struct LendingMarketMetadata { 26 | /// Bump seed 27 | pub bump_seed: u8, 28 | /// Market name null padded 29 | pub market_name: [u8; MARKET_NAME_SIZE], 30 | /// Market description null padded 31 | pub market_description: [u8; MARKET_DESCRIPTION_SIZE], 32 | /// Market image url 33 | pub market_image_url: [u8; MARKET_IMAGE_URL_SIZE], 34 | /// Lookup Tables 35 | pub lookup_tables: [Pubkey; 4], 36 | /// Padding 37 | pub padding: [u8; PADDING_SIZE], 38 | } 39 | 40 | impl LendingMarketMetadata { 41 | /// Create a LendingMarketMetadata referernce from a slice 42 | pub fn new_from_bytes(data: &[u8]) -> Result<&LendingMarketMetadata, ProgramError> { 43 | try_from_bytes::(&data[1..]).map_err(|_| { 44 | msg!("Failed to deserialize LendingMarketMetadata"); 45 | LendingError::InstructionUnpackError.into() 46 | }) 47 | } 48 | } 49 | 50 | unsafe impl Zeroable for LendingMarketMetadata {} 51 | unsafe impl Pod for LendingMarketMetadata {} 52 | 53 | assert_eq_size!( 54 | LendingMarketMetadata, 55 | [u8; MARKET_NAME_SIZE 56 | + MARKET_DESCRIPTION_SIZE 57 | + MARKET_IMAGE_URL_SIZE 58 | + 4 * 32 59 | + PADDING_SIZE 60 | + 1], 61 | ); 62 | 63 | // transaction size limit check 64 | const_assert!(std::mem::size_of::() <= 850); 65 | -------------------------------------------------------------------------------- /patch.crates-io.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Patches workspace crates for developing against a local solana monorepo 4 | # 5 | 6 | solana_dir=$1 7 | if [[ -z $solana_dir ]]; then 8 | echo "Usage: $0 " 9 | exit 1 10 | fi 11 | 12 | workspace_crates=( 13 | Cargo.toml 14 | ) 15 | 16 | if [[ ! -r "$solana_dir"/scripts/read-cargo-variable.sh ]]; then 17 | echo "$solana_dir is not a path to the solana monorepo" 18 | exit 1 19 | fi 20 | 21 | set -e 22 | 23 | solana_dir=$(cd "$solana_dir" && pwd) 24 | cd "$(dirname "$0")" 25 | 26 | # shellcheck source=/dev/null 27 | source "$solana_dir"/scripts/read-cargo-variable.sh 28 | solana_ver=$(readCargoVariable version "$solana_dir"/sdk/Cargo.toml) 29 | 30 | echo "Patching in $solana_ver from $solana_dir" 31 | echo 32 | for crate in "${workspace_crates[@]}"; do 33 | if grep -q '\[patch.crates-io\]' "$crate"; then 34 | echo "$crate is already patched" 35 | else 36 | cat >> "$crate" <, 16 | } 17 | 18 | pub fn get_transaction_balance_change( 19 | rpc_client: &RpcClient, 20 | signature: &Signature, 21 | address: &Pubkey, 22 | address_is_token: bool, 23 | ) -> Result> { 24 | let confirmed_transaction = 25 | rpc_client.get_transaction(signature, UiTransactionEncoding::Base64)?; 26 | 27 | let slot = confirmed_transaction.slot; 28 | let when = confirmed_transaction 29 | .block_time 30 | .map(|block_time| { 31 | #[allow(deprecated)] 32 | NaiveDateTime::from_timestamp_opt(block_time, 0) 33 | .ok_or_else(|| format!("Invalid block time for slot {slot}")) 34 | }) 35 | .transpose()?; 36 | 37 | let meta = confirmed_transaction 38 | .transaction 39 | .meta 40 | .ok_or("Transaction metadata not available")?; 41 | 42 | if meta.err.is_some() { 43 | return Err("Transaction was not successful".into()); 44 | } 45 | 46 | let transaction = confirmed_transaction 47 | .transaction 48 | .transaction 49 | .decode() 50 | .ok_or("Unable to decode transaction")?; 51 | 52 | let account_index = transaction 53 | .message 54 | .static_account_keys() 55 | .iter() 56 | .position(|k| k == address) 57 | .ok_or_else(|| format!("Address {address} not referenced in transaction"))?; 58 | 59 | let pre_amount = if address_is_token { 60 | u64::from_str( 61 | &Option::>::from(meta.pre_token_balances) 62 | .unwrap() 63 | .iter() 64 | .find(|ptb| ptb.account_index as usize == account_index) 65 | .unwrap() 66 | .ui_token_amount 67 | .amount, 68 | ) 69 | .unwrap_or_default() 70 | } else { 71 | meta.pre_balances[account_index] 72 | }; 73 | 74 | let post_amount = if address_is_token { 75 | u64::from_str( 76 | &Option::>::from(meta.post_token_balances) 77 | .unwrap() 78 | .iter() 79 | .find(|ptb| ptb.account_index as usize == account_index) 80 | .unwrap() 81 | .ui_token_amount 82 | .amount, 83 | ) 84 | .unwrap_or_default() 85 | } else { 86 | meta.post_balances[account_index] 87 | }; 88 | 89 | Ok(GetTransactionAddrssBalanceChange { 90 | pre_amount, 91 | post_amount, 92 | slot, 93 | when, 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Sell Your ◎ 2 | 3 | `sys` provides tracking from acquisition through disposal of SOL from staking, voting, and validator transaction fee/rent rewards, useful 4 | for portfolio tracking as well as producing the necessary records for proper tax preparation. 5 | 6 | The intended audience for this program is: 7 | 1. Solana Validators that need to track voting and transaction fee/rent rewards 8 | 2. Solana Stakers that need to track staking rewards 9 | 10 | This program does not attempt to be a general purpose crypto trading tracker. 11 | It's assumed that once you sell your SOL for USD on an exchange of your choice, 12 | you'd switch to other existing solutions for further trading/transactions. That 13 | being said, it also includes support for Jupiter token swaps with a curated set 14 | of tokens. 15 | 16 | Additionally the `sys-lend` companion program provides a unified command-line 17 | interface into the top Solana lending platforms to easily earn yield for when 18 | you're not quite ready to part with your SOL, USDC, or other tokens yet. 19 | 20 | ## Quick Start 21 | 1. Install Rust from https://rustup.rs/ 22 | 2. `cargo run` 23 | 3. `cargo run --bin sys-lend` 24 | 25 | You can also run `./fetch-release.sh` to download the latest Linux and macOS binary produced by Github Actions. 26 | 27 | ## Features 28 | * Exchange deposit integration with Coinbase, Kraken, Binance and Binance US 29 | * Fetch market info, SOL balance and sell order status 30 | * Deposit from a vote, stake or system account 31 | * Initiate and cancel basic limit orders 32 | * Jupiter Aggregator token swaps between supported tokens 33 | * Automatic epoch reward tracking for vote and stake accounts 34 | * Validator identity rewards are also automatically tracked at the epoch level, but not directly attributed to each individual block that rewards are credited 35 | * Lot management for all tracked accounts, with income and long/short capital gain/loss tracking suitable for tax prep purposes 36 | * A _sweep stake account_ system, whereby vote account rewards can be automatically swept into a stake account and staked as quickly as possible 37 | * Historical and spot price via CoinGecko for SOL and supported tokens. 38 | * Data is contained in a local `sell-your-sol/` subdirectory that can be easily backed up, and is editable by hand if necessary 39 | * Full Excel export, useful to hand off to a CPA or your entity's finance department. Sorry no TurboTax import! 40 | * Companion `sys-lend` program for easy stablecoin and memecoin lending into MarginFi, Kamino, Drift and Solend 41 | 42 | ## Examples 43 | Explore the help system instead: 44 | ``` 45 | $ sys --help 46 | $ sys-lend --help 47 | ``` 48 | 49 | It aims to be self explanatory. If not feel free to ask, or better yet send a PR to improve the situation 50 | 51 | ## Limitations 52 | * No FMV discount is computed for locked stake rewards 53 | * Accounts under `sys` management should not be manipulated outside of `sys`. For example `sys` will get confused if you split some stake using the `solana` command-line tool, and probably assert 54 | * The US tax system is assumed in several of the commands, extending to other jurisdictions should be doable and would be a welcome contribution 55 | * You may have to write code to fix bugs or implement new features that are not required in my workflow 56 | -------------------------------------------------------------------------------- /src/helius_rpc.rs: -------------------------------------------------------------------------------- 1 | use {solana_client::rpc_client::RpcClient, solana_sdk::instruction::Instruction}; 2 | 3 | #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)] 4 | #[serde(rename_all = "camelCase")] 5 | pub enum HeliusPriorityLevel { 6 | None, // 0th percentile 7 | Low, // 25th percentile 8 | Medium, // 50th percentile 9 | High, // 75th percentile 10 | VeryHigh, // 95th percentile 11 | Default, // 50th percentile 12 | } 13 | 14 | pub type HeliusMicroLamportPriorityFee = f64; 15 | 16 | #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] 17 | pub struct HeliusGetPriorityFeeEstimateRequest { 18 | pub transaction: Option, // estimate fee for a serialized txn 19 | pub account_keys: Option>, // estimate fee for a list of accounts 20 | pub options: Option, 21 | } 22 | 23 | #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] 24 | pub struct HeliusGetPriorityFeeEstimateOptions { 25 | pub priority_level: Option, // Default to MEDIUM 26 | pub include_all_priority_fee_levels: Option, // Include all priority level estimates in the response 27 | pub transaction_encoding: Option, // Default Base58 28 | pub lookback_slots: Option, // number of slots to look back to calculate estimate. Valid number are 1-150, default is 150 29 | } 30 | 31 | #[derive(serde::Deserialize, Debug)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct HeliusGetPriorityFeeEstimateResponse { 34 | pub priority_fee_estimate: Option, 35 | pub priority_fee_levels: Option, 36 | } 37 | 38 | #[derive(serde::Deserialize, Debug)] 39 | pub struct HeliusMicroLamportPriorityFeeLevels { 40 | pub none: f64, 41 | pub low: f64, 42 | pub medium: f64, 43 | pub high: f64, 44 | pub very_high: f64, 45 | pub unsafe_max: f64, 46 | } 47 | 48 | pub fn get_priority_fee_estimate_for_instructions( 49 | rpc_client: &RpcClient, 50 | priority_level: HeliusPriorityLevel, 51 | instructions: &[Instruction], 52 | ) -> Result { 53 | let mut account_keys: Vec<_> = instructions 54 | .iter() 55 | .flat_map(|instruction| { 56 | instruction 57 | .accounts 58 | .iter() 59 | .map(|account_meta| account_meta.pubkey.to_string()) 60 | .collect::>() 61 | }) 62 | .collect(); 63 | account_keys.sort(); 64 | account_keys.dedup(); 65 | 66 | let request = serde_json::json!([HeliusGetPriorityFeeEstimateRequest { 67 | options: Some(HeliusGetPriorityFeeEstimateOptions { 68 | priority_level: Some(priority_level), 69 | lookback_slots: Some(50), // ~25s seconds 70 | ..HeliusGetPriorityFeeEstimateOptions::default() 71 | }), 72 | account_keys: Some(account_keys), 73 | ..HeliusGetPriorityFeeEstimateRequest::default() 74 | }]); 75 | 76 | rpc_client 77 | .send::( 78 | solana_client::rpc_request::RpcRequest::Custom { 79 | method: "getPriorityFeeEstimate", 80 | }, 81 | request, 82 | ) 83 | .map(|response| { 84 | response 85 | .priority_fee_estimate 86 | .expect("priority_fee_estimate") as u64 87 | }) 88 | .map_err(|err| format!("Failed to invoke RPC method getPriorityFeeEstimate: {err}")) 89 | } 90 | -------------------------------------------------------------------------------- /src/metrics.rs: -------------------------------------------------------------------------------- 1 | pub use influxdb_client::{Client, Point}; 2 | use { 3 | chrono::Utc, 4 | influxdb_client::{timestamp, Precision, Timestamp, TimestampOptions}, 5 | serde::{Deserialize, Serialize}, 6 | std::{env, sync::Arc}, 7 | tokio::sync::RwLock, 8 | }; 9 | 10 | lazy_static::lazy_static! { 11 | static ref POINTS: Arc>> = Arc::new(RwLock::new(vec![])); 12 | } 13 | 14 | #[derive(Debug, Serialize, Deserialize)] 15 | pub struct MetricsConfig { 16 | pub url: String, 17 | pub token: String, 18 | pub org: String, 19 | pub bucket: String, 20 | } 21 | 22 | pub fn env_config() -> Option { 23 | Some(MetricsConfig { 24 | url: env::var("INFLUX_URL").ok()?, 25 | token: env::var("INFLUX_API_TOKEN").ok()?, 26 | org: env::var("INFLUX_ORG").unwrap_or_default(), 27 | bucket: env::var("INFLUX_BUCKET") 28 | .ok() 29 | .unwrap_or_else(|| "sys".into()), 30 | }) 31 | } 32 | 33 | pub async fn push(point: Point) { 34 | POINTS.write().await.push(point); 35 | } 36 | 37 | pub async fn send(config: Option) { 38 | if let Some(config) = config { 39 | let client = Client::new(config.url, config.token) 40 | .with_org(config.org) 41 | .with_bucket(config.bucket) 42 | .with_precision(Precision::MS); 43 | //let client = client.insert_to_stdout(); 44 | 45 | // Write all metrics with the same timestamp to ensure multiple sys-lend APY and balance 46 | // values line up 47 | let timestamp = timestamp!(Utc::now().timestamp_millis()); 48 | client 49 | .insert_points(&*POINTS.write().await, timestamp) 50 | .await 51 | .unwrap_or_else(|err| eprintln!("Failed to send metrics: {err:?}")); 52 | } 53 | } 54 | 55 | pub mod dp { 56 | use { 57 | crate::{ 58 | exchange::{Exchange, OrderSide}, 59 | token::MaybeToken, 60 | }, 61 | influxdb_client::{Point, Value}, 62 | solana_sdk::pubkey::Pubkey, 63 | }; 64 | 65 | pub fn pubkey_to_value(p: &Pubkey) -> Value { 66 | Value::Str(p.to_string()) 67 | } 68 | 69 | pub fn exchange_deposit(exchange: Exchange, maybe_token: MaybeToken, ui_amount: f64) -> Point { 70 | Point::new("exchange_deposit") 71 | .tag("exchange", exchange.to_string().as_str()) 72 | .tag("token", maybe_token.name()) 73 | .field("amount", ui_amount) 74 | } 75 | 76 | pub fn exchange_withdrawal( 77 | exchange: Exchange, 78 | maybe_token: MaybeToken, 79 | address: &Pubkey, 80 | ui_amount: f64, 81 | ) -> Point { 82 | Point::new("exchange_withdrawal") 83 | .tag("exchange", exchange.to_string().as_str()) 84 | .tag("token", maybe_token.name()) 85 | .tag("address", pubkey_to_value(address)) 86 | .field("amount", ui_amount) 87 | } 88 | 89 | pub fn exchange_fill( 90 | exchange: Exchange, 91 | pair: &str, 92 | side: OrderSide, 93 | maybe_token: MaybeToken, 94 | amount: f64, 95 | price: f64, 96 | ) -> Point { 97 | Point::new("exchange_fill") 98 | .tag("exchange", exchange.to_string().as_str()) 99 | .tag("pair", pair) 100 | .tag("side", side.to_string().as_str()) 101 | .tag("token", maybe_token.name()) 102 | .field("price", price) 103 | .field("amount", amount) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/vendor/kamino/borrow_rate_curve.rs: -------------------------------------------------------------------------------- 1 | use super::fraction::{Fraction, FractionExtra}; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 4 | #[repr(C, packed(1))] 5 | pub struct BorrowRateCurve { 6 | pub points: [CurvePoint; 11], 7 | } 8 | 9 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 10 | pub struct CurveSegment { 11 | pub slope_nom: u32, 12 | pub slope_denom: u32, 13 | pub start_point: CurvePoint, 14 | } 15 | 16 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 17 | #[repr(C, packed(1))] 18 | pub struct CurvePoint { 19 | pub utilization_rate_bps: u32, 20 | pub borrow_rate_bps: u32, 21 | } 22 | 23 | impl CurvePoint { 24 | pub fn new(utilization_rate_bps: u32, borrow_rate_bps: u32) -> Self { 25 | Self { 26 | utilization_rate_bps, 27 | borrow_rate_bps, 28 | } 29 | } 30 | } 31 | 32 | impl CurveSegment { 33 | pub fn from_points(start: CurvePoint, end: CurvePoint) -> Option { 34 | let slope_nom = end.borrow_rate_bps.checked_sub(start.borrow_rate_bps)?; 35 | if end.utilization_rate_bps <= start.utilization_rate_bps { 36 | //msg!("Utilization rate must be ever growing in the curve"); 37 | return None; 38 | } 39 | let slope_denom = end 40 | .utilization_rate_bps 41 | .checked_sub(start.utilization_rate_bps) 42 | .unwrap(); 43 | 44 | Some(CurveSegment { 45 | slope_nom, 46 | slope_denom, 47 | start_point: start, 48 | }) 49 | } 50 | 51 | pub(self) fn get_borrow_rate(&self, utilization_rate: Fraction) -> Option { 52 | let start_utilization_rate = Fraction::from_bps(self.start_point.utilization_rate_bps); 53 | 54 | let coef = utilization_rate.checked_sub(start_utilization_rate)?; 55 | 56 | let nom = coef * u128::from(self.slope_nom); 57 | let base_rate = nom / u128::from(self.slope_denom); 58 | 59 | let offset = Fraction::from_bps(self.start_point.borrow_rate_bps); 60 | 61 | Some(base_rate + offset) 62 | } 63 | } 64 | 65 | impl BorrowRateCurve { 66 | pub fn get_borrow_rate(&self, utilization_rate: Fraction) -> Option { 67 | let utilization_rate = if utilization_rate > Fraction::ONE { 68 | /* 69 | msg!( 70 | "Warning: utilization rate is greater than 100% (scaled): {}", 71 | utilization_rate.to_bits() 72 | ); 73 | */ 74 | Fraction::ONE 75 | } else { 76 | utilization_rate 77 | }; 78 | 79 | let utilization_rate_bps: u32 = utilization_rate.to_bps().unwrap(); 80 | 81 | let (start_pt, end_pt) = self 82 | .points 83 | .windows(2) 84 | .map(|seg| { 85 | let [first, second]: &[CurvePoint; 2] = seg.try_into().unwrap(); 86 | (first, second) 87 | }) 88 | .find(|(first, second)| { 89 | utilization_rate_bps >= first.utilization_rate_bps 90 | && utilization_rate_bps <= second.utilization_rate_bps 91 | }) 92 | .unwrap(); 93 | if utilization_rate_bps == start_pt.utilization_rate_bps { 94 | return Some(Fraction::from_bps(start_pt.borrow_rate_bps)); 95 | } else if utilization_rate_bps == end_pt.utilization_rate_bps { 96 | return Some(Fraction::from_bps(end_pt.borrow_rate_bps)); 97 | } 98 | 99 | let segment = CurveSegment::from_points(*start_pt, *end_pt)?; 100 | 101 | segment.get_borrow_rate(utilization_rate) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/vendor/drift/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod spot_market; 2 | pub mod user; 3 | 4 | use {spot_market::SpotMarket, user::SpotBalanceType}; 5 | 6 | pub const ONE_YEAR: u128 = 31536000; 7 | 8 | pub const PERCENTAGE_PRECISION: u128 = 1_000_000; // expo -6 (represents 100%) 9 | pub const PERCENTAGE_PRECISION_I128: i128 = PERCENTAGE_PRECISION as i128; 10 | pub const PERCENTAGE_PRECISION_U64: u64 = PERCENTAGE_PRECISION as u64; 11 | pub const PERCENTAGE_PRECISION_I64: i64 = PERCENTAGE_PRECISION as i64; 12 | 13 | pub const SPOT_BALANCE_PRECISION: u128 = 1_000_000_000; // expo = -9 14 | //pub const SPOT_BALANCE_PRECISION_U64: u64 = 1_000_000_000; // expo = -9 15 | pub const SPOT_CUMULATIVE_INTEREST_PRECISION: u128 = 10_000_000_000; // expo = -10 16 | 17 | pub const SPOT_UTILIZATION_PRECISION: u128 = PERCENTAGE_PRECISION; // expo = -6 18 | pub const SPOT_UTILIZATION_PRECISION_U32: u32 = PERCENTAGE_PRECISION as u32; // expo = -6 19 | pub const SPOT_RATE_PRECISION: u128 = PERCENTAGE_PRECISION; // expo = -6 20 | pub const SPOT_RATE_PRECISION_U32: u32 = PERCENTAGE_PRECISION as u32; // expo = -6 21 | 22 | pub fn token_amount_to_scaled_balance( 23 | token_amount: u64, 24 | spot_market: &SpotMarket, 25 | balance_type: SpotBalanceType, 26 | ) -> u64 { 27 | let precision_increase = 10_u128.pow(19_u32.saturating_sub(spot_market.decimals)); 28 | 29 | let cumulative_interest = match balance_type { 30 | SpotBalanceType::Deposit => spot_market.cumulative_deposit_interest, 31 | SpotBalanceType::Borrow => spot_market.cumulative_borrow_interest, 32 | }; 33 | 34 | ((token_amount as u128) * precision_increase / cumulative_interest) as u64 35 | } 36 | 37 | pub fn scaled_balance_to_token_amount( 38 | scaled_balance: u128, 39 | spot_market: &SpotMarket, 40 | balance_type: SpotBalanceType, 41 | ) -> u64 { 42 | let precision_increase = 10_u128.pow(19_u32.saturating_sub(spot_market.decimals)); 43 | 44 | let cumulative_interest = match balance_type { 45 | SpotBalanceType::Deposit => spot_market.cumulative_deposit_interest, 46 | SpotBalanceType::Borrow => spot_market.cumulative_borrow_interest, 47 | }; 48 | 49 | (scaled_balance * cumulative_interest / precision_increase) as u64 50 | } 51 | 52 | pub fn calculate_utilization(deposit_token_amount: u64, borrow_token_amount: u64) -> u128 { 53 | (borrow_token_amount as u128 * SPOT_UTILIZATION_PRECISION) 54 | .checked_div(deposit_token_amount as u128) 55 | .unwrap_or({ 56 | if deposit_token_amount == 0 && borrow_token_amount == 0 { 57 | 0_u128 58 | } else { 59 | // if there are borrows without deposits, default to maximum utilization rate 60 | SPOT_UTILIZATION_PRECISION 61 | } 62 | }) 63 | } 64 | 65 | pub fn calculate_spot_market_utilization(spot_market: &SpotMarket) -> u128 { 66 | let deposit_token_amount = scaled_balance_to_token_amount( 67 | spot_market.deposit_balance, 68 | spot_market, 69 | SpotBalanceType::Deposit, 70 | ); 71 | let borrow_token_amount = scaled_balance_to_token_amount( 72 | spot_market.borrow_balance, 73 | spot_market, 74 | SpotBalanceType::Borrow, 75 | ); 76 | calculate_utilization(deposit_token_amount, borrow_token_amount) 77 | } 78 | 79 | #[derive(Default, Debug)] 80 | pub struct InterestRate { 81 | pub borrow_rate: f64, 82 | pub deposit_rate: f64, 83 | } 84 | 85 | pub fn calculate_accumulated_interest(spot_market: &SpotMarket) -> InterestRate { 86 | let utilization = calculate_spot_market_utilization(spot_market); 87 | 88 | if utilization == 0 { 89 | InterestRate::default() 90 | } else { 91 | let borrow_rate = if utilization > spot_market.optimal_utilization as u128 { 92 | let surplus_utilization = 93 | utilization.saturating_sub(spot_market.optimal_utilization as u128); 94 | 95 | let borrow_rate_slope = (spot_market.max_borrow_rate as u128) 96 | .saturating_sub(spot_market.optimal_borrow_rate as u128) 97 | * SPOT_UTILIZATION_PRECISION 98 | / SPOT_UTILIZATION_PRECISION 99 | .saturating_sub(spot_market.optimal_utilization as u128); 100 | 101 | (spot_market.optimal_borrow_rate as u128) 102 | + (surplus_utilization * borrow_rate_slope / SPOT_UTILIZATION_PRECISION) 103 | } else { 104 | let borrow_rate_slope = (spot_market.optimal_borrow_rate as u128) 105 | * SPOT_UTILIZATION_PRECISION 106 | / (spot_market.optimal_utilization as u128); 107 | 108 | utilization * borrow_rate_slope / SPOT_UTILIZATION_PRECISION 109 | }; 110 | 111 | let deposit_rate = borrow_rate * utilization / SPOT_UTILIZATION_PRECISION; 112 | 113 | InterestRate { 114 | borrow_rate: borrow_rate as f64 / PERCENTAGE_PRECISION as f64, 115 | deposit_rate: deposit_rate as f64 / PERCENTAGE_PRECISION as f64, 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/vendor/solend/math/rate.rs: -------------------------------------------------------------------------------- 1 | //! Math for preserving precision of ratios and percentages. 2 | //! 3 | //! Usages and their ranges include: 4 | //! - Collateral exchange ratio <= 5.0 5 | //! - Loan to value ratio <= 0.9 6 | //! - Max borrow rate <= 2.56 7 | //! - Percentages <= 1.0 8 | //! 9 | //! Rates are internally scaled by a WAD (10^18) to preserve 10 | //! precision up to 18 decimal places. Rates are sized to support 11 | //! both serialization and precise math for the full range of 12 | //! unsigned 8-bit integers. The underlying representation is a 13 | //! u128 rather than u192 to reduce compute cost while losing 14 | //! support for arithmetic operations at the high end of u8 range. 15 | 16 | #![allow(clippy::assign_op_pattern)] 17 | #![allow(clippy::ptr_offset_with_cast)] 18 | #![allow(clippy::reversed_empty_ranges)] 19 | #![allow(clippy::manual_range_contains)] 20 | #![allow(clippy::manual_div_ceil)] 21 | 22 | use crate::vendor::solend::{ 23 | error::LendingError, 24 | math::{common::*, decimal::Decimal}, 25 | }; 26 | use solana_program::program_error::ProgramError; 27 | use std::{convert::TryFrom, fmt}; 28 | use uint::construct_uint; 29 | 30 | // U128 with 128 bits consisting of 2 x 64-bit words 31 | construct_uint! { 32 | pub struct U128(2); 33 | } 34 | 35 | /// Small decimal values, precise to 18 digits 36 | #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Eq, Ord)] 37 | pub struct Rate(pub U128); 38 | 39 | impl Rate { 40 | /// One 41 | pub fn one() -> Self { 42 | Self(Self::wad()) 43 | } 44 | 45 | /// Zero 46 | pub fn zero() -> Self { 47 | Self(U128::from(0)) 48 | } 49 | 50 | // OPTIMIZE: use const slice when fixed in BPF toolchain 51 | fn wad() -> U128 { 52 | U128::from(WAD) 53 | } 54 | 55 | /// Create scaled decimal from percent value 56 | pub fn from_percent(percent: u8) -> Self { 57 | Self(U128::from(percent as u64 * PERCENT_SCALER)) 58 | } 59 | 60 | /// Create scaled decimal from percent value 61 | pub fn from_percent_u64(percent: u64) -> Self { 62 | Self(U128::from(percent) * PERCENT_SCALER) 63 | } 64 | 65 | /// Return raw scaled value 66 | #[allow(clippy::wrong_self_convention)] 67 | pub fn to_scaled_val(&self) -> u128 { 68 | self.0.as_u128() 69 | } 70 | 71 | /// Create decimal from scaled value 72 | pub fn from_scaled_val(scaled_val: u64) -> Self { 73 | Self(U128::from(scaled_val)) 74 | } 75 | 76 | /// Calculates base^exp 77 | pub fn try_pow(&self, mut exp: u64) -> Result { 78 | let mut base = *self; 79 | let mut ret = if !exp.is_multiple_of(2) { 80 | base 81 | } else { 82 | Rate(Self::wad()) 83 | }; 84 | 85 | while exp > 0 { 86 | exp /= 2; 87 | base = base.try_mul(base)?; 88 | 89 | if !exp.is_multiple_of(2) { 90 | ret = ret.try_mul(base)?; 91 | } 92 | } 93 | 94 | Ok(ret) 95 | } 96 | } 97 | 98 | impl fmt::Display for Rate { 99 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 100 | let mut scaled_val = self.0.to_string(); 101 | if scaled_val.len() <= SCALE { 102 | scaled_val.insert_str(0, &vec!["0"; SCALE - scaled_val.len()].join("")); 103 | scaled_val.insert_str(0, "0."); 104 | } else { 105 | scaled_val.insert(scaled_val.len() - SCALE, '.'); 106 | } 107 | f.write_str(&scaled_val) 108 | } 109 | } 110 | 111 | impl TryFrom for Rate { 112 | type Error = ProgramError; 113 | fn try_from(decimal: Decimal) -> Result { 114 | Ok(Self(U128::from(decimal.to_scaled_val()?))) 115 | } 116 | } 117 | 118 | impl TryAdd for Rate { 119 | fn try_add(self, rhs: Self) -> Result { 120 | Ok(Self( 121 | self.0 122 | .checked_add(rhs.0) 123 | .ok_or(LendingError::MathOverflow)?, 124 | )) 125 | } 126 | } 127 | 128 | impl TrySub for Rate { 129 | fn try_sub(self, rhs: Self) -> Result { 130 | Ok(Self( 131 | self.0 132 | .checked_sub(rhs.0) 133 | .ok_or(LendingError::MathOverflow)?, 134 | )) 135 | } 136 | } 137 | 138 | impl TryDiv for Rate { 139 | fn try_div(self, rhs: u64) -> Result { 140 | Ok(Self( 141 | self.0 142 | .checked_div(U128::from(rhs)) 143 | .ok_or(LendingError::MathOverflow)?, 144 | )) 145 | } 146 | } 147 | 148 | impl TryDiv for Rate { 149 | fn try_div(self, rhs: Self) -> Result { 150 | Ok(Self( 151 | self.0 152 | .checked_mul(Self::wad()) 153 | .ok_or(LendingError::MathOverflow)? 154 | .checked_div(rhs.0) 155 | .ok_or(LendingError::MathOverflow)?, 156 | )) 157 | } 158 | } 159 | 160 | impl TryMul for Rate { 161 | fn try_mul(self, rhs: u64) -> Result { 162 | Ok(Self( 163 | self.0 164 | .checked_mul(U128::from(rhs)) 165 | .ok_or(LendingError::MathOverflow)?, 166 | )) 167 | } 168 | } 169 | 170 | impl TryMul for Rate { 171 | fn try_mul(self, rhs: Self) -> Result { 172 | Ok(Self( 173 | self.0 174 | .checked_mul(rhs.0) 175 | .ok_or(LendingError::MathOverflow)? 176 | .checked_div(Self::wad()) 177 | .ok_or(LendingError::MathOverflow)?, 178 | )) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/rpc_client_utils.rs: -------------------------------------------------------------------------------- 1 | use { 2 | chrono::prelude::*, 3 | solana_client::rpc_client::RpcClient, 4 | solana_pubkey::Pubkey, 5 | solana_sdk::{ 6 | account::Account, 7 | account_utils::StateMut, 8 | clock::Slot, 9 | signature::Signature, 10 | stake::state::{Authorized, StakeStateV2}, 11 | }, 12 | }; 13 | 14 | #[derive(Clone, Debug, PartialEq)] 15 | pub enum StakeActivationState { 16 | Activating, 17 | Active, 18 | Deactivating, 19 | Inactive, 20 | } 21 | 22 | pub async fn get_block_date( 23 | rpc_client: &RpcClient, 24 | slot: Slot, 25 | ) -> Result> { 26 | let block_time = rpc_client.get_block_time(slot)?; 27 | let local_timestamp = Local.timestamp_opt(block_time, 0).unwrap(); 28 | Ok(NaiveDate::from_ymd_opt( 29 | local_timestamp.year(), 30 | local_timestamp.month(), 31 | local_timestamp.day(), 32 | ) 33 | .unwrap()) 34 | } 35 | 36 | pub fn get_stake_activation_state( 37 | rpc_client: &RpcClient, 38 | stake_account: &Account, 39 | ) -> Result> { 40 | let stake_state = stake_account 41 | .state() 42 | .map_err(|err| format!("Failed to get account state: {err}"))?; 43 | let stake_history_account = rpc_client.get_account(&solana_sdk::sysvar::stake_history::id())?; 44 | let stake_history: solana_sdk::stake_history::StakeHistory = 45 | solana_sdk::account::from_account(&stake_history_account).unwrap(); 46 | let clock_account = rpc_client.get_account(&solana_sdk::sysvar::clock::id())?; 47 | let clock: solana_sdk::clock::Clock = 48 | solana_sdk::account::from_account(&clock_account).unwrap(); 49 | let new_rate_activation_epoch = rpc_client 50 | .get_feature_activation_slot(&solana_sdk::feature_set::reduce_stake_warmup_cooldown::id()) 51 | .and_then(|activation_slot: Option| { 52 | rpc_client 53 | .get_epoch_schedule() 54 | .map(|epoch_schedule| (activation_slot, epoch_schedule)) 55 | }) 56 | .map(|(activation_slot, epoch_schedule)| { 57 | activation_slot.map(|slot| epoch_schedule.get_epoch(slot)) 58 | })?; 59 | 60 | if let solana_sdk::stake::state::StakeStateV2::Stake(_, stake, _) = stake_state { 61 | let solana_sdk::stake::state::StakeActivationStatus { 62 | effective, 63 | activating, 64 | deactivating, 65 | } = stake.delegation.stake_activating_and_deactivating( 66 | clock.epoch, 67 | &stake_history, 68 | new_rate_activation_epoch, 69 | ); 70 | if effective == 0 { 71 | return Ok(StakeActivationState::Inactive); 72 | } 73 | if activating > 0 { 74 | return Ok(StakeActivationState::Activating); 75 | } 76 | if deactivating > 0 { 77 | return Ok(StakeActivationState::Deactivating); 78 | } 79 | return Ok(StakeActivationState::Active); 80 | } 81 | Err("No stake".to_string().into()) 82 | } 83 | 84 | pub fn get_stake_authorized( 85 | rpc_client: &RpcClient, 86 | stake_account_address: Pubkey, 87 | ) -> Result<(Authorized, Pubkey), Box> { 88 | let stake_account = rpc_client.get_account(&stake_account_address)?; 89 | 90 | match get_stake_activation_state(rpc_client, &stake_account)? { 91 | StakeActivationState::Active | StakeActivationState::Activating => {} 92 | state => { 93 | return Err(format!( 94 | "Stake account {stake_account_address} must be Active or Activating: {state:?}" 95 | ) 96 | .into()); 97 | } 98 | } 99 | 100 | match stake_account.state() { 101 | Ok(StakeStateV2::Stake(meta, stake, _stake_flags)) => { 102 | Ok((meta.authorized, stake.delegation.voter_pubkey)) 103 | } 104 | _ => Err(format!("Invalid stake account: {stake_account_address}").into()), 105 | } 106 | } 107 | 108 | pub fn stake_accounts_have_same_credits_observed( 109 | stake_account1: &Account, 110 | stake_account2: &Account, 111 | ) -> Result> { 112 | use solana_sdk::stake::state::Stake; 113 | 114 | let stake_state1 = bincode::deserialize(stake_account1.data.as_slice()) 115 | .map_err(|err| format!("Invalid stake account 1: {err}"))?; 116 | let stake_state2 = bincode::deserialize(stake_account2.data.as_slice()) 117 | .map_err(|err| format!("Invalid stake account 2: {err}"))?; 118 | 119 | if let ( 120 | StakeStateV2::Stake( 121 | _, 122 | Stake { 123 | delegation: _, 124 | credits_observed: credits_observed1, 125 | }, 126 | _, 127 | ), 128 | StakeStateV2::Stake( 129 | _, 130 | Stake { 131 | delegation: _, 132 | credits_observed: credits_observed2, 133 | }, 134 | _, 135 | ), 136 | ) = (stake_state1, stake_state2) 137 | { 138 | return Ok(credits_observed1 == credits_observed2); 139 | } 140 | Ok(false) 141 | } 142 | 143 | pub async fn get_signature_date( 144 | rpc_client: &RpcClient, 145 | signature: Signature, 146 | ) -> Result> { 147 | let statuses = rpc_client.get_signature_statuses_with_history(&[signature])?; 148 | if let Some(Some(ts)) = statuses.value.first() { 149 | let block_date = get_block_date(rpc_client, ts.slot).await?; 150 | Ok(block_date) 151 | } else { 152 | Err(format!("Unknown signature: {signature}").into()) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/coinbase_exchange.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{exchange::*, token::MaybeToken}, 3 | async_trait::async_trait, 4 | futures::{pin_mut, stream::StreamExt}, 5 | rust_decimal::prelude::*, 6 | solana_sdk::pubkey::Pubkey, 7 | std::collections::HashMap, 8 | }; 9 | 10 | pub struct CoinbaseExchangeClient { 11 | client: coinbase_rs::Private, 12 | } 13 | 14 | #[async_trait] 15 | impl ExchangeClient for CoinbaseExchangeClient { 16 | async fn deposit_address( 17 | &self, 18 | token: MaybeToken, 19 | ) -> Result> { 20 | let accounts = self.client.accounts(); 21 | pin_mut!(accounts); 22 | 23 | while let Some(account_result) = accounts.next().await { 24 | for account in account_result.unwrap() { 25 | if let Ok(id) = coinbase_rs::Uuid::from_str(&account.id) { 26 | if token.name() == account.currency.code 27 | && account.primary 28 | && account.allow_deposits 29 | { 30 | let addresses = self.client.list_addresses(&id); 31 | pin_mut!(addresses); 32 | 33 | let mut best_pubkey_updated_at = None; 34 | let mut best_pubkey = None; 35 | while let Some(addresses_result) = addresses.next().await { 36 | for address in addresses_result.unwrap() { 37 | if address.network.as_str() == "solana" { 38 | if let Ok(pubkey) = address.address.parse::() { 39 | if address.updated_at > best_pubkey_updated_at { 40 | best_pubkey_updated_at = address.updated_at; 41 | best_pubkey = Some(pubkey); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | if let Some(pubkey) = best_pubkey { 48 | return Ok(pubkey); 49 | } 50 | break; 51 | } 52 | } 53 | } 54 | } 55 | Err(format!("Unsupported deposit token: {}", token.name()).into()) 56 | } 57 | 58 | async fn balances( 59 | &self, 60 | ) -> Result, Box> { 61 | Err("Balances not supported".into()) 62 | } 63 | 64 | async fn recent_deposits( 65 | &self, 66 | ) -> Result>, Box> { 67 | Ok(None) // TODO: Return actual recent deposits. By returning `None`, deposited lots are dropped 68 | // once the transaction is confirmed (see `db::drop_deposit()`). 69 | } 70 | 71 | async fn recent_withdrawals(&self) -> Result, Box> { 72 | Ok(vec![]) 73 | } 74 | 75 | async fn request_withdraw( 76 | &self, 77 | _address: Pubkey, 78 | _token: MaybeToken, 79 | _amount: f64, 80 | _password: Option, 81 | _code: Option, 82 | ) -> Result<(/* withdraw_id: */ String, /*withdraw_fee: */ f64), Box> 83 | { 84 | Err("Withdrawals not supported".into()) 85 | } 86 | 87 | async fn print_market_info( 88 | &self, 89 | _pair: &str, 90 | _format: MarketInfoFormat, 91 | ) -> Result<(), Box> { 92 | Err("Quotes not supported".into()) 93 | } 94 | 95 | async fn bid_ask(&self, _pair: &str) -> Result> { 96 | Err("Trading not supported".into()) 97 | } 98 | 99 | async fn place_order( 100 | &self, 101 | _pair: &str, 102 | _side: OrderSide, 103 | _price: f64, 104 | _amount: f64, 105 | ) -> Result> { 106 | Err("Trading not supported".into()) 107 | } 108 | 109 | async fn cancel_order( 110 | &self, 111 | _pair: &str, 112 | _order_id: &OrderId, 113 | ) -> Result<(), Box> { 114 | Err("Trading not supported".into()) 115 | } 116 | 117 | async fn order_status( 118 | &self, 119 | _pair: &str, 120 | _order_id: &OrderId, 121 | ) -> Result> { 122 | Err("Trading not supported".into()) 123 | } 124 | 125 | async fn get_lending_info( 126 | &self, 127 | _coin: &str, 128 | ) -> Result, Box> { 129 | Err("Lending not supported".into()) 130 | } 131 | 132 | async fn get_lending_history( 133 | &self, 134 | _lending_history: LendingHistory, 135 | ) -> Result, Box> { 136 | Err("Lending not supported".into()) 137 | } 138 | 139 | async fn submit_lending_offer( 140 | &self, 141 | _coin: &str, 142 | _size: f64, 143 | ) -> Result<(), Box> { 144 | Err("Lending not supported".into()) 145 | } 146 | 147 | fn preferred_solusd_pair(&self) -> &'static str { 148 | "SOLUSD" 149 | } 150 | } 151 | 152 | pub fn new( 153 | ExchangeCredentials { 154 | api_key, 155 | secret, 156 | subaccount, 157 | }: ExchangeCredentials, 158 | ) -> Result> { 159 | assert!(subaccount.is_none()); 160 | Ok(CoinbaseExchangeClient { 161 | client: coinbase_rs::Private::new(coinbase_rs::MAIN_URL, &api_key, &secret), 162 | }) 163 | } 164 | -------------------------------------------------------------------------------- /src/exchange.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{binance_exchange, coinbase_exchange, kraken_exchange, token::MaybeToken}, 3 | async_trait::async_trait, 4 | chrono::NaiveDate, 5 | serde::{Deserialize, Serialize}, 6 | solana_sdk::pubkey::Pubkey, 7 | std::{collections::HashMap, str::FromStr}, 8 | thiserror::Error, 9 | }; 10 | 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 12 | pub enum Exchange { 13 | Binance, 14 | BinanceUs, 15 | Coinbase, 16 | Ftx, 17 | FtxUs, 18 | Kraken, 19 | } 20 | 21 | impl std::fmt::Display for Exchange { 22 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 23 | write!(f, "{self:?}") 24 | } 25 | } 26 | 27 | pub const USD_COINS: &[&str] = &["USD", "USDC", "USDT", "BUSD", "ZUSD"]; 28 | 29 | impl FromStr for Exchange { 30 | type Err = ParseExchangeError; 31 | 32 | fn from_str(s: &str) -> Result { 33 | match s { 34 | "Binance" | "binance" => Ok(Exchange::Binance), 35 | "BinanceUs" | "binanceus" => Ok(Exchange::BinanceUs), 36 | "Coinbase" | "coinbase" => Ok(Exchange::Coinbase), 37 | "Kraken" | "kraken" => Ok(Exchange::Kraken), 38 | _ => Err(ParseExchangeError::InvalidExchange), 39 | } 40 | } 41 | } 42 | 43 | #[derive(Error, Debug)] 44 | pub enum ParseExchangeError { 45 | #[error("invalid exchange")] 46 | InvalidExchange, 47 | } 48 | 49 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 50 | pub struct ExchangeCredentials { 51 | pub api_key: String, 52 | pub secret: String, 53 | pub subaccount: Option, 54 | } 55 | 56 | #[derive(Debug, Default, Clone)] 57 | pub struct ExchangeBalance { 58 | pub available: f64, 59 | pub total: f64, 60 | } 61 | 62 | #[derive(Debug)] 63 | pub struct DepositInfo { 64 | pub tx_id: String, 65 | pub amount: f64, // TODO: rename to `ui_amount` 66 | } 67 | 68 | #[derive(Debug)] 69 | pub struct WithdrawalInfo { 70 | pub address: Pubkey, 71 | pub token: MaybeToken, 72 | pub amount: f64, // TODO: rename to `ui_amount` 73 | pub tag: String, 74 | 75 | pub completed: bool, // when `completed`, a `tx_id` of `None` indicates a cancelled withdrawal 76 | pub tx_id: Option, 77 | } 78 | 79 | #[derive(Debug)] 80 | pub struct BidAsk { 81 | pub bid_price: f64, 82 | pub ask_price: f64, 83 | } 84 | 85 | pub type OrderId = String; 86 | 87 | #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] 88 | pub enum OrderSide { 89 | Buy, 90 | Sell, 91 | } 92 | 93 | impl std::fmt::Display for OrderSide { 94 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 95 | write!(f, "{self:?}") 96 | } 97 | } 98 | 99 | #[derive(Debug)] 100 | pub struct OrderStatus { 101 | pub open: bool, 102 | pub side: OrderSide, 103 | pub price: f64, 104 | pub amount: f64, 105 | pub filled_amount: f64, 106 | pub last_update: NaiveDate, 107 | pub fee: Option<(f64, String)>, 108 | } 109 | 110 | #[derive(PartialEq, Eq)] 111 | pub enum MarketInfoFormat { 112 | All, 113 | Ask, 114 | Weighted24hAveragePrice, 115 | Hourly, 116 | } 117 | 118 | pub struct LendingInfo { 119 | pub lendable: f64, 120 | pub offered: f64, 121 | pub locked: f64, 122 | pub estimate_rate: f64, // estimated lending rate for the next spot margin cycle 123 | pub previous_rate: f64, // lending rate in the previous spot margin cycle 124 | } 125 | 126 | pub enum LendingHistory { 127 | Range { 128 | start_date: NaiveDate, 129 | end_date: NaiveDate, 130 | }, 131 | Previous { 132 | days: usize, 133 | }, 134 | } 135 | 136 | #[async_trait] 137 | pub trait ExchangeClient { 138 | async fn deposit_address( 139 | &self, 140 | token: MaybeToken, 141 | ) -> Result>; 142 | async fn recent_deposits(&self) 143 | -> Result>, Box>; 144 | async fn recent_withdrawals(&self) -> Result, Box>; 145 | async fn request_withdraw( 146 | &self, 147 | address: Pubkey, 148 | token: MaybeToken, 149 | amount: f64, 150 | withdrawal_password: Option, 151 | withdrawal_code: Option, 152 | ) -> Result<(/* withdraw_id: */ String, /*withdraw_fee: */ f64), Box>; 153 | async fn balances( 154 | &self, 155 | ) -> Result, Box>; 156 | async fn print_market_info( 157 | &self, 158 | pair: &str, 159 | format: MarketInfoFormat, 160 | ) -> Result<(), Box>; 161 | async fn bid_ask(&self, pair: &str) -> Result>; 162 | async fn place_order( 163 | &self, 164 | pair: &str, 165 | side: OrderSide, 166 | price: f64, 167 | amount: f64, 168 | ) -> Result>; 169 | #[allow(clippy::ptr_arg)] 170 | async fn cancel_order( 171 | &self, 172 | pair: &str, 173 | order_id: &OrderId, 174 | ) -> Result<(), Box>; 175 | #[allow(clippy::ptr_arg)] 176 | async fn order_status( 177 | &self, 178 | pair: &str, 179 | order_id: &OrderId, 180 | ) -> Result>; 181 | async fn get_lending_info( 182 | &self, 183 | coin: &str, 184 | ) -> Result, Box>; 185 | async fn get_lending_history( 186 | &self, 187 | lending_history: LendingHistory, 188 | ) -> Result, Box>; 189 | async fn submit_lending_offer( 190 | &self, 191 | coin: &str, 192 | size: f64, 193 | ) -> Result<(), Box>; 194 | fn preferred_solusd_pair(&self) -> &'static str; 195 | } 196 | 197 | pub fn exchange_client_new( 198 | exchange: Exchange, 199 | exchange_credentials: ExchangeCredentials, 200 | ) -> Result, Box> { 201 | let exchange_client: Box = match exchange { 202 | Exchange::Binance => Box::new(binance_exchange::new(exchange_credentials)?), 203 | Exchange::BinanceUs => Box::new(binance_exchange::new_us(exchange_credentials)?), 204 | Exchange::Coinbase => Box::new(coinbase_exchange::new(exchange_credentials)?), 205 | Exchange::Kraken => Box::new(kraken_exchange::new(exchange_credentials)?), 206 | Exchange::Ftx | Exchange::FtxUs => return Err("Unsupported Exchange".into()), 207 | }; 208 | Ok(exchange_client) 209 | } 210 | -------------------------------------------------------------------------------- /src/vendor/kamino/reserve.rs: -------------------------------------------------------------------------------- 1 | pub use fixed::types::U68F60 as Fraction; 2 | use { 3 | super::{ 4 | borrow_rate_curve::BorrowRateCurve, fraction::FractionExtra, last_update::LastUpdate, 5 | token_info::TokenInfo, 6 | }, 7 | solana_sdk::pubkey::Pubkey, 8 | }; 9 | 10 | #[derive(Default, Debug, Clone, Copy)] 11 | #[repr(C, packed(1))] 12 | pub struct BigFractionBytes { 13 | pub value: [u64; 4], 14 | pub padding: [u64; 2], 15 | } 16 | 17 | static_assertions::const_assert_eq!(8616, std::mem::size_of::()); 18 | static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); 19 | #[derive(Debug, Clone, Copy)] 20 | #[repr(C, packed(1))] 21 | pub struct Reserve { 22 | pub version: u64, 23 | 24 | pub last_update: LastUpdate, 25 | 26 | pub lending_market: Pubkey, 27 | 28 | pub farm_collateral: Pubkey, 29 | pub farm_debt: Pubkey, 30 | 31 | pub liquidity: ReserveLiquidity, 32 | 33 | pub reserve_liquidity_padding: [u64; 150], 34 | 35 | pub collateral: ReserveCollateral, 36 | 37 | pub reserve_collateral_padding: [u64; 150], 38 | 39 | pub config: ReserveConfig, 40 | 41 | pub config_padding: [u64; 150], 42 | 43 | pub padding: [u64; 240], 44 | } 45 | 46 | impl Reserve { 47 | pub fn current_supply_apr(&self) -> f64 { 48 | let utilization_rate = self.liquidity.utilization_rate(); 49 | let protocol_take_rate_pct = self.config.protocol_take_rate_pct as f64 / 100.; 50 | 51 | let current_borrow_rate = self 52 | .config 53 | .borrow_rate_curve 54 | .get_borrow_rate(utilization_rate) 55 | .unwrap_or(Fraction::ZERO); 56 | 57 | (utilization_rate 58 | * current_borrow_rate 59 | * (Fraction::ONE - Fraction::from_num(protocol_take_rate_pct))) 60 | .checked_to_num() 61 | .unwrap_or(0.) 62 | } 63 | 64 | pub fn collateral_exchange_rate(&self) -> CollateralExchangeRate { 65 | let total_liquidity = self.liquidity.total_supply(); 66 | self.collateral.exchange_rate(total_liquidity) 67 | } 68 | } 69 | 70 | #[derive(Debug, Clone, Copy)] 71 | #[repr(C, packed(1))] 72 | pub struct ReserveLiquidity { 73 | pub mint_pubkey: Pubkey, 74 | pub supply_vault: Pubkey, 75 | pub fee_vault: Pubkey, 76 | pub available_amount: u64, 77 | pub borrowed_amount_sf: u128, 78 | pub market_price_sf: u128, 79 | pub market_price_last_updated_ts: u64, 80 | pub mint_decimals: u64, 81 | 82 | pub deposit_limit_crossed_slot: u64, 83 | pub borrow_limit_crossed_slot: u64, 84 | 85 | pub cumulative_borrow_rate_bsf: BigFractionBytes, 86 | pub accumulated_protocol_fees_sf: u128, 87 | pub accumulated_referrer_fees_sf: u128, 88 | pub pending_referrer_fees_sf: u128, 89 | pub absolute_referral_rate_sf: u128, 90 | 91 | pub padding2: [u64; 55], 92 | pub padding3: [u128; 32], 93 | } 94 | 95 | impl ReserveLiquidity { 96 | pub fn total_supply(&self) -> Fraction { 97 | Fraction::from(self.available_amount) + Fraction::from_bits(self.borrowed_amount_sf) 98 | - Fraction::from_bits(self.accumulated_protocol_fees_sf) 99 | - Fraction::from_bits(self.accumulated_referrer_fees_sf) 100 | - Fraction::from_bits(self.pending_referrer_fees_sf) 101 | } 102 | 103 | pub fn total_borrow(&self) -> Fraction { 104 | Fraction::from_bits(self.borrowed_amount_sf) 105 | } 106 | 107 | pub fn utilization_rate(&self) -> Fraction { 108 | let total_supply = self.total_supply(); 109 | if total_supply == Fraction::ZERO { 110 | return Fraction::ZERO; 111 | } 112 | Fraction::from_bits(self.borrowed_amount_sf) / total_supply 113 | } 114 | } 115 | 116 | #[derive(Debug, Clone, Copy)] 117 | #[repr(C, packed(1))] 118 | pub struct ReserveCollateral { 119 | pub mint_pubkey: Pubkey, 120 | pub mint_total_supply: u64, 121 | pub supply_vault: Pubkey, 122 | pub padding1: [u128; 32], 123 | pub padding2: [u128; 32], 124 | } 125 | 126 | impl ReserveCollateral { 127 | fn exchange_rate(&self, total_liquidity: Fraction) -> CollateralExchangeRate { 128 | let rate = if self.mint_total_supply == 0 || total_liquidity == Fraction::ZERO { 129 | Fraction::ONE 130 | } else { 131 | Fraction::from(self.mint_total_supply) / total_liquidity 132 | }; 133 | 134 | CollateralExchangeRate(rate) 135 | } 136 | } 137 | 138 | #[derive(Clone, Copy, Debug)] 139 | pub struct CollateralExchangeRate(Fraction); 140 | 141 | impl CollateralExchangeRate { 142 | pub fn collateral_to_liquidity(&self, collateral_amount: u64) -> u64 { 143 | self.fraction_collateral_to_liquidity(collateral_amount.into()) 144 | .to_floor() 145 | } 146 | 147 | pub fn fraction_collateral_to_liquidity(&self, collateral_amount: Fraction) -> Fraction { 148 | collateral_amount / self.0 149 | } 150 | 151 | pub fn liquidity_to_collateral(&self, liquidity_amount: u64) -> u64 { 152 | (self.0 * u128::from(liquidity_amount)).to_floor() 153 | } 154 | } 155 | 156 | static_assertions::const_assert_eq!(648, std::mem::size_of::()); 157 | static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); 158 | #[derive(Debug, Clone, Copy)] 159 | #[repr(C, packed(1))] 160 | pub struct ReserveConfig { 161 | pub status: u8, 162 | pub asset_tier: u8, 163 | pub reserved_0: [u8; 2], 164 | pub multiplier_side_boost: [u8; 2], 165 | pub multiplier_tag_boost: [u8; 8], 166 | pub protocol_take_rate_pct: u8, 167 | pub protocol_liquidation_fee_pct: u8, 168 | pub loan_to_value_pct: u8, 169 | pub liquidation_threshold_pct: u8, 170 | pub min_liquidation_bonus_bps: u16, 171 | pub max_liquidation_bonus_bps: u16, 172 | pub bad_debt_liquidation_bonus_bps: u16, 173 | pub deleveraging_margin_call_period_secs: u64, 174 | pub deleveraging_threshold_slots_per_bps: u64, 175 | pub fees: ReserveFees, 176 | pub borrow_rate_curve: BorrowRateCurve, 177 | pub borrow_factor_pct: u64, 178 | 179 | pub deposit_limit: u64, 180 | pub borrow_limit: u64, 181 | pub token_info: TokenInfo, 182 | 183 | pub deposit_withdrawal_cap: WithdrawalCaps, 184 | pub debt_withdrawal_cap: WithdrawalCaps, 185 | 186 | pub elevation_groups: [u8; 20], 187 | pub reserved_1: [u8; 4], 188 | } 189 | 190 | #[derive(Debug, Clone, Copy)] 191 | #[repr(C, packed(1))] 192 | pub struct WithdrawalCaps { 193 | pub config_capacity: i64, 194 | pub current_total: i64, 195 | pub last_interval_start_timestamp: u64, 196 | pub config_interval_length_seconds: u64, 197 | } 198 | 199 | #[derive(Debug, Clone, Copy)] 200 | #[repr(C, packed(1))] 201 | pub struct ReserveFees { 202 | pub borrow_fee_sf: u64, 203 | pub flash_loan_fee_sf: u64, 204 | pub padding: [u8; 8], 205 | } 206 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | solana_clap_utils::input_validators::normalize_to_url_if_moniker, 3 | solana_client::{ 4 | rpc_client::{RpcClient, SerializableTransaction}, 5 | rpc_response, 6 | }, 7 | solana_sdk::{clock::Slot, commitment_config::CommitmentConfig}, 8 | std::{ 9 | thread::sleep, 10 | time::{Duration, Instant}, 11 | }, 12 | }; 13 | 14 | pub mod amount; 15 | pub mod binance_exchange; 16 | pub mod coin_gecko; 17 | pub mod coinbase_exchange; 18 | pub mod db; 19 | pub mod exchange; 20 | pub mod field_as_string; 21 | pub mod get_transaction_balance_change; 22 | pub mod helius_rpc; 23 | pub mod kraken_exchange; 24 | pub mod metrics; 25 | pub mod notifier; 26 | pub mod priority_fee; 27 | pub mod rpc_client_utils; 28 | pub mod token; 29 | pub mod vendor; 30 | 31 | pub fn app_version() -> String { 32 | let tag = option_env!("GITHUB_REF") 33 | .and_then(|github_ref| github_ref.strip_prefix("refs/tags/").map(|s| s.to_string())); 34 | 35 | tag.unwrap_or_else(|| match option_env!("GITHUB_SHA") { 36 | None => "devbuild".to_string(), 37 | Some(commit) => commit[..8].to_string(), 38 | }) 39 | } 40 | 41 | pub fn is_comma_separated_url_or_moniker_list(string: T) -> Result<(), String> 42 | where 43 | T: AsRef + std::fmt::Display, 44 | { 45 | for url_or_moniker in string.as_ref().split(',') { 46 | solana_clap_utils::input_validators::is_url_or_moniker(url_or_moniker)?; 47 | } 48 | 49 | Ok(()) 50 | } 51 | 52 | pub struct RpcClients { 53 | clients: Vec<(String, RpcClient)>, 54 | helius: Option, 55 | } 56 | 57 | impl RpcClients { 58 | pub fn new( 59 | json_rpc_url: String, 60 | send_json_rpc_urls: Option, 61 | helius: Option, 62 | ) -> Self { 63 | let mut json_rpc_urls = vec![json_rpc_url]; 64 | if let Some(send_json_rpc_urls) = send_json_rpc_urls { 65 | for send_json_rpc_url in send_json_rpc_urls.split(',') { 66 | json_rpc_urls.push(send_json_rpc_url.into()); 67 | } 68 | } 69 | 70 | Self { 71 | clients: json_rpc_urls 72 | .into_iter() 73 | .map(|json_rpc_url| { 74 | let json_rpc_url = normalize_to_url_if_moniker(json_rpc_url); 75 | ( 76 | json_rpc_url.clone(), 77 | RpcClient::new_with_commitment(json_rpc_url, CommitmentConfig::confirmed()), 78 | ) 79 | }) 80 | .collect(), 81 | helius: helius.map(|helius_json_rpc_url| { 82 | RpcClient::new_with_commitment(helius_json_rpc_url, CommitmentConfig::confirmed()) 83 | }), 84 | } 85 | } 86 | 87 | pub fn default(&self) -> &RpcClient { 88 | &self.clients[0].1 89 | } 90 | 91 | pub fn helius_or_default(&self) -> &RpcClient { 92 | self.helius 93 | .as_ref() 94 | .map_or_else(|| self.default(), |helius| helius) 95 | } 96 | } 97 | 98 | // Assumes `transaction` has already been signed and simulated... 99 | pub fn send_transaction_until_expired( 100 | rpc_clients: &RpcClients, 101 | transaction: &impl SerializableTransaction, 102 | last_valid_block_height: u64, 103 | ) -> Option { 104 | send_transaction_until_expired_with_slot(rpc_clients, transaction, last_valid_block_height) 105 | .map(|(_context_slot, success)| success) 106 | } 107 | 108 | // Same as `send_transaction_until_expired` but on success returns a `Slot` that the transaction 109 | // was observed to be confirmed at 110 | fn send_transaction_until_expired_with_slot( 111 | rpc_clients: &RpcClients, 112 | transaction: &impl SerializableTransaction, 113 | last_valid_block_height: u64, 114 | ) -> Option<(Slot, bool)> { 115 | let mut last_send_attempt = None; 116 | 117 | loop { 118 | if last_send_attempt.is_none() 119 | || Instant::now() 120 | .duration_since(*last_send_attempt.as_ref().unwrap()) 121 | .as_secs() 122 | > 2 123 | { 124 | for (json_rpc_url, rpc_client) in rpc_clients.clients.iter().rev() { 125 | println!( 126 | "Sending transaction {} [{json_rpc_url}]", 127 | transaction.get_signature() 128 | ); 129 | 130 | if let Err(err) = rpc_client.send_transaction(transaction) { 131 | println!("Unable to send transaction: {err:?}"); 132 | } 133 | } 134 | last_send_attempt = Some(Instant::now()); 135 | } 136 | 137 | sleep(Duration::from_millis(500)); 138 | 139 | match rpc_clients 140 | .default() 141 | .get_signature_statuses(&[*transaction.get_signature()]) 142 | { 143 | Ok(rpc_response::Response { context, value }) => { 144 | let confirmation_context_slot = context.slot; 145 | if let Some(ref transaction_status) = value[0] { 146 | return Some(( 147 | confirmation_context_slot, 148 | match transaction_status.err { 149 | None => true, 150 | Some(ref err) => { 151 | println!("Transaction failed: {err}"); 152 | false 153 | } 154 | }, 155 | )); 156 | } else { 157 | match rpc_clients.default().get_epoch_info() { 158 | Ok(epoch_info) => { 159 | if epoch_info.block_height > last_valid_block_height 160 | && epoch_info.absolute_slot >= confirmation_context_slot 161 | { 162 | println!( 163 | "Transaction expired as of slot {confirmation_context_slot}" 164 | ); 165 | return None; 166 | } 167 | println!( 168 | "(transaction unconfirmed as of slot {}, {} blocks until expiry)", 169 | confirmation_context_slot, 170 | last_valid_block_height.saturating_sub(epoch_info.block_height), 171 | ); 172 | } 173 | Err(err) => { 174 | println!("Unable to get epoch info: {err:?}") 175 | } 176 | } 177 | } 178 | } 179 | Err(err) => { 180 | println!("Unable to get transaction status: {err:?}"); 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/vendor/solend/math/decimal.rs: -------------------------------------------------------------------------------- 1 | //! Math for preserving precision of token amounts which are limited 2 | //! by the SPL Token program to be at most u64::MAX. 3 | //! 4 | //! Decimals are internally scaled by a WAD (10^18) to preserve 5 | //! precision up to 18 decimal places. Decimals are sized to support 6 | //! both serialization and precise math for the full range of 7 | //! unsigned 64-bit integers. The underlying representation is a 8 | //! u192 rather than u256 to reduce compute cost while losing 9 | //! support for arithmetic operations at the high end of u64 range. 10 | 11 | #![allow(clippy::assign_op_pattern)] 12 | #![allow(clippy::ptr_offset_with_cast)] 13 | #![allow(clippy::manual_range_contains)] 14 | #![allow(clippy::manual_div_ceil)] 15 | 16 | use crate::vendor::solend::{ 17 | error::LendingError, 18 | math::{common::*, Rate}, 19 | }; 20 | use solana_program::program_error::ProgramError; 21 | use std::{convert::TryFrom, fmt}; 22 | use uint::construct_uint; 23 | 24 | // U192 with 192 bits consisting of 3 x 64-bit words 25 | construct_uint! { 26 | pub struct U192(3); 27 | } 28 | 29 | /// Large decimal values, precise to 18 digits 30 | #[derive(Clone, Copy, Default, PartialEq, PartialOrd, Eq, Ord)] 31 | pub struct Decimal(pub U192); 32 | 33 | impl Decimal { 34 | /// One 35 | pub fn one() -> Self { 36 | Self(Self::wad()) 37 | } 38 | 39 | /// Zero 40 | pub fn zero() -> Self { 41 | Self(U192::zero()) 42 | } 43 | 44 | // OPTIMIZE: use const slice when fixed in BPF toolchain 45 | fn wad() -> U192 { 46 | U192::from(WAD) 47 | } 48 | 49 | // OPTIMIZE: use const slice when fixed in BPF toolchain 50 | fn half_wad() -> U192 { 51 | U192::from(HALF_WAD) 52 | } 53 | 54 | /// Create scaled decimal from percent value 55 | pub fn from_percent(percent: u8) -> Self { 56 | Self(U192::from(percent as u64 * PERCENT_SCALER)) 57 | } 58 | 59 | /// Create scaled decimal from deca bps value 60 | pub fn from_deca_bps(deca_bps: u8) -> Self { 61 | Self::from(deca_bps as u64).try_div(1000).unwrap() 62 | } 63 | 64 | /// Create scaled decimal from bps value 65 | pub fn from_bps(bps: u64) -> Self { 66 | Self::from(bps).try_div(10_000).unwrap() 67 | } 68 | 69 | /// Return raw scaled value if it fits within u128 70 | #[allow(clippy::wrong_self_convention)] 71 | pub fn to_scaled_val(&self) -> Result { 72 | Ok(u128::try_from(self.0).map_err(|_| LendingError::MathOverflow)?) 73 | } 74 | 75 | /// Create decimal from scaled value 76 | pub fn from_scaled_val(scaled_val: u128) -> Self { 77 | Self(U192::from(scaled_val)) 78 | } 79 | 80 | /// Round scaled decimal to u64 81 | pub fn try_round_u64(&self) -> Result { 82 | let rounded_val = Self::half_wad() 83 | .checked_add(self.0) 84 | .ok_or(LendingError::MathOverflow)? 85 | .checked_div(Self::wad()) 86 | .ok_or(LendingError::MathOverflow)?; 87 | Ok(u64::try_from(rounded_val).map_err(|_| LendingError::MathOverflow)?) 88 | } 89 | 90 | /// Ceiling scaled decimal to u64 91 | pub fn try_ceil_u64(&self) -> Result { 92 | let ceil_val = Self::wad() 93 | .checked_sub(U192::from(1u64)) 94 | .ok_or(LendingError::MathOverflow)? 95 | .checked_add(self.0) 96 | .ok_or(LendingError::MathOverflow)? 97 | .checked_div(Self::wad()) 98 | .ok_or(LendingError::MathOverflow)?; 99 | Ok(u64::try_from(ceil_val).map_err(|_| LendingError::MathOverflow)?) 100 | } 101 | 102 | /// Floor scaled decimal to u64 103 | pub fn try_floor_u64(&self) -> Result { 104 | let ceil_val = self 105 | .0 106 | .checked_div(Self::wad()) 107 | .ok_or(LendingError::MathOverflow)?; 108 | Ok(u64::try_from(ceil_val).map_err(|_| LendingError::MathOverflow)?) 109 | } 110 | } 111 | 112 | impl fmt::Display for Decimal { 113 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 114 | let mut scaled_val = self.0.to_string(); 115 | if scaled_val.len() <= SCALE { 116 | scaled_val.insert_str(0, &vec!["0"; SCALE - scaled_val.len()].join("")); 117 | scaled_val.insert_str(0, "0."); 118 | } else { 119 | scaled_val.insert(scaled_val.len() - SCALE, '.'); 120 | } 121 | f.write_str(&scaled_val) 122 | } 123 | } 124 | 125 | impl fmt::Debug for Decimal { 126 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 127 | write!(f, "{self}") 128 | } 129 | } 130 | 131 | impl From for Decimal { 132 | fn from(val: u64) -> Self { 133 | Self(Self::wad() * U192::from(val)) 134 | } 135 | } 136 | 137 | impl From for Decimal { 138 | fn from(val: u128) -> Self { 139 | Self(Self::wad() * U192::from(val)) 140 | } 141 | } 142 | 143 | impl From for Decimal { 144 | fn from(val: Rate) -> Self { 145 | Self(U192::from(val.to_scaled_val())) 146 | } 147 | } 148 | 149 | impl TryAdd for Decimal { 150 | fn try_add(self, rhs: Self) -> Result { 151 | Ok(Self( 152 | self.0 153 | .checked_add(rhs.0) 154 | .ok_or(LendingError::MathOverflow)?, 155 | )) 156 | } 157 | } 158 | 159 | impl TrySub for Decimal { 160 | fn try_sub(self, rhs: Self) -> Result { 161 | Ok(Self( 162 | self.0 163 | .checked_sub(rhs.0) 164 | .ok_or(LendingError::MathOverflow)?, 165 | )) 166 | } 167 | } 168 | 169 | impl TryDiv for Decimal { 170 | fn try_div(self, rhs: u64) -> Result { 171 | Ok(Self( 172 | self.0 173 | .checked_div(U192::from(rhs)) 174 | .ok_or(LendingError::MathOverflow)?, 175 | )) 176 | } 177 | } 178 | 179 | impl TryDiv for Decimal { 180 | fn try_div(self, rhs: Rate) -> Result { 181 | self.try_div(Self::from(rhs)) 182 | } 183 | } 184 | 185 | impl TryDiv for Decimal { 186 | fn try_div(self, rhs: Self) -> Result { 187 | Ok(Self( 188 | self.0 189 | .checked_mul(Self::wad()) 190 | .ok_or(LendingError::MathOverflow)? 191 | .checked_div(rhs.0) 192 | .ok_or(LendingError::MathOverflow)?, 193 | )) 194 | } 195 | } 196 | 197 | impl TryMul for Decimal { 198 | fn try_mul(self, rhs: u64) -> Result { 199 | Ok(Self( 200 | self.0 201 | .checked_mul(U192::from(rhs)) 202 | .ok_or(LendingError::MathOverflow)?, 203 | )) 204 | } 205 | } 206 | 207 | impl TryMul for Decimal { 208 | fn try_mul(self, rhs: Rate) -> Result { 209 | self.try_mul(Self::from(rhs)) 210 | } 211 | } 212 | 213 | impl TryMul for Decimal { 214 | fn try_mul(self, rhs: Self) -> Result { 215 | Ok(Self( 216 | self.0 217 | .checked_mul(rhs.0) 218 | .ok_or(LendingError::MathOverflow)? 219 | .checked_div(Self::wad()) 220 | .ok_or(LendingError::MathOverflow)?, 221 | )) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/vendor/solend/state/lending_market.rs: -------------------------------------------------------------------------------- 1 | use crate::vendor::solend::state::*; 2 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 3 | use solana_program::{ 4 | msg, 5 | program_error::ProgramError, 6 | program_pack::{IsInitialized, Pack, Sealed}, 7 | pubkey::{Pubkey, PUBKEY_BYTES}, 8 | }; 9 | 10 | /// Lending market state 11 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 12 | pub struct LendingMarket { 13 | /// Version of lending market 14 | pub version: u8, 15 | /// Bump seed for derived authority address 16 | pub bump_seed: u8, 17 | /// Owner authority which can add new reserves 18 | pub owner: Pubkey, 19 | /// Currency market prices are quoted in 20 | /// e.g. "USD" null padded (`*b"USD\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"`) or a SPL token mint pubkey 21 | pub quote_currency: [u8; 32], 22 | /// Token program id 23 | pub token_program_id: Pubkey, 24 | /// Oracle (Pyth) program id 25 | pub oracle_program_id: Pubkey, 26 | /// Oracle (Switchboard) program id 27 | pub switchboard_oracle_program_id: Pubkey, 28 | /// Outflow rate limiter denominated in dollars 29 | pub rate_limiter: RateLimiter, 30 | /// whitelisted liquidator 31 | pub whitelisted_liquidator: Option, 32 | /// risk authority (additional pubkey used for setting params) 33 | pub risk_authority: Pubkey, 34 | } 35 | 36 | impl LendingMarket { 37 | /// Create a new lending market 38 | pub fn new(params: InitLendingMarketParams) -> Self { 39 | let mut lending_market = Self::default(); 40 | Self::init(&mut lending_market, params); 41 | lending_market 42 | } 43 | 44 | /// Initialize a lending market 45 | pub fn init(&mut self, params: InitLendingMarketParams) { 46 | self.version = PROGRAM_VERSION; 47 | self.bump_seed = params.bump_seed; 48 | self.owner = params.owner; 49 | self.quote_currency = params.quote_currency; 50 | self.token_program_id = params.token_program_id; 51 | self.oracle_program_id = params.oracle_program_id; 52 | self.switchboard_oracle_program_id = params.switchboard_oracle_program_id; 53 | self.rate_limiter = RateLimiter::default(); 54 | self.whitelisted_liquidator = None; 55 | self.risk_authority = params.owner; 56 | } 57 | } 58 | 59 | /// Initialize a lending market 60 | pub struct InitLendingMarketParams { 61 | /// Bump seed for derived authority address 62 | pub bump_seed: u8, 63 | /// Owner authority which can add new reserves 64 | pub owner: Pubkey, 65 | /// Currency market prices are quoted in 66 | /// e.g. "USD" null padded (`*b"USD\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"`) or a SPL token mint pubkey 67 | pub quote_currency: [u8; 32], 68 | /// Token program id 69 | pub token_program_id: Pubkey, 70 | /// Oracle (Pyth) program id 71 | pub oracle_program_id: Pubkey, 72 | /// Oracle (Switchboard) program id 73 | pub switchboard_oracle_program_id: Pubkey, 74 | } 75 | 76 | impl Sealed for LendingMarket {} 77 | impl IsInitialized for LendingMarket { 78 | fn is_initialized(&self) -> bool { 79 | self.version != UNINITIALIZED_VERSION 80 | } 81 | } 82 | 83 | const LENDING_MARKET_LEN: usize = 290; // 1 + 1 + 32 + 32 + 32 + 32 + 32 + 56 + 32 + 40 84 | impl Pack for LendingMarket { 85 | const LEN: usize = LENDING_MARKET_LEN; 86 | 87 | fn pack_into_slice(&self, output: &mut [u8]) { 88 | let output = array_mut_ref![output, 0, LENDING_MARKET_LEN]; 89 | #[allow(clippy::ptr_offset_with_cast)] 90 | let ( 91 | version, 92 | bump_seed, 93 | owner, 94 | quote_currency, 95 | token_program_id, 96 | oracle_program_id, 97 | switchboard_oracle_program_id, 98 | rate_limiter, 99 | whitelisted_liquidator, 100 | risk_authority, 101 | _padding, 102 | ) = mut_array_refs![ 103 | output, 104 | 1, 105 | 1, 106 | PUBKEY_BYTES, 107 | 32, 108 | PUBKEY_BYTES, 109 | PUBKEY_BYTES, 110 | PUBKEY_BYTES, 111 | RATE_LIMITER_LEN, 112 | PUBKEY_BYTES, 113 | PUBKEY_BYTES, 114 | 8 115 | ]; 116 | 117 | *version = self.version.to_le_bytes(); 118 | *bump_seed = self.bump_seed.to_le_bytes(); 119 | owner.copy_from_slice(self.owner.as_ref()); 120 | quote_currency.copy_from_slice(self.quote_currency.as_ref()); 121 | token_program_id.copy_from_slice(self.token_program_id.as_ref()); 122 | oracle_program_id.copy_from_slice(self.oracle_program_id.as_ref()); 123 | switchboard_oracle_program_id.copy_from_slice(self.switchboard_oracle_program_id.as_ref()); 124 | self.rate_limiter.pack_into_slice(rate_limiter); 125 | match self.whitelisted_liquidator { 126 | Some(pubkey) => { 127 | whitelisted_liquidator.copy_from_slice(pubkey.as_ref()); 128 | } 129 | None => { 130 | whitelisted_liquidator.copy_from_slice(&[0u8; 32]); 131 | } 132 | } 133 | risk_authority.copy_from_slice(self.risk_authority.as_ref()); 134 | } 135 | 136 | /// Unpacks a byte buffer into a [LendingMarketInfo](struct.LendingMarketInfo.html) 137 | fn unpack_from_slice(input: &[u8]) -> Result { 138 | let input = array_ref![input, 0, LENDING_MARKET_LEN]; 139 | #[allow(clippy::ptr_offset_with_cast)] 140 | let ( 141 | version, 142 | bump_seed, 143 | owner, 144 | quote_currency, 145 | token_program_id, 146 | oracle_program_id, 147 | switchboard_oracle_program_id, 148 | rate_limiter, 149 | whitelisted_liquidator, 150 | risk_authority, 151 | _padding, 152 | ) = array_refs![ 153 | input, 154 | 1, 155 | 1, 156 | PUBKEY_BYTES, 157 | 32, 158 | PUBKEY_BYTES, 159 | PUBKEY_BYTES, 160 | PUBKEY_BYTES, 161 | RATE_LIMITER_LEN, 162 | PUBKEY_BYTES, 163 | PUBKEY_BYTES, 164 | 8 165 | ]; 166 | 167 | let version = u8::from_le_bytes(*version); 168 | if version > PROGRAM_VERSION { 169 | msg!("Lending market version does not match lending program version"); 170 | return Err(ProgramError::InvalidAccountData); 171 | } 172 | 173 | let owner_pubkey = Pubkey::new_from_array(*owner); 174 | Ok(Self { 175 | version, 176 | bump_seed: u8::from_le_bytes(*bump_seed), 177 | owner: owner_pubkey, 178 | quote_currency: *quote_currency, 179 | token_program_id: Pubkey::new_from_array(*token_program_id), 180 | oracle_program_id: Pubkey::new_from_array(*oracle_program_id), 181 | switchboard_oracle_program_id: Pubkey::new_from_array(*switchboard_oracle_program_id), 182 | rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, 183 | whitelisted_liquidator: if whitelisted_liquidator == &[0u8; 32] { 184 | None 185 | } else { 186 | Some(Pubkey::new_from_array(*whitelisted_liquidator)) 187 | }, 188 | // the risk authority can equal [0; 32] when the program is upgraded to v2.0.2. in that 189 | // case, we set the risk authority to be the owner. This isn't strictly necessary, but 190 | // better to be safe i guess. 191 | risk_authority: if *risk_authority == [0; 32] { 192 | owner_pubkey 193 | } else { 194 | Pubkey::new_from_array(*risk_authority) 195 | }, 196 | }) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/coin_gecko.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::token::{MaybeToken, Token}, 3 | chrono::prelude::*, 4 | rust_decimal::prelude::*, 5 | serde::{Deserialize, Serialize}, 6 | std::{collections::HashMap, env, sync::Arc}, 7 | tokio::sync::RwLock, 8 | }; 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | struct CurrencyList { 12 | usd: f64, 13 | } 14 | 15 | #[derive(Debug, Serialize, Deserialize)] 16 | struct MarketData { 17 | current_price: CurrencyList, 18 | market_cap: CurrencyList, 19 | total_volume: CurrencyList, 20 | } 21 | 22 | // This `derive` requires the `serde` dependency. 23 | #[derive(Debug, Serialize, Deserialize)] 24 | struct HistoryResponse { 25 | id: String, 26 | name: String, 27 | symbol: String, 28 | market_data: Option, 29 | } 30 | 31 | fn token_to_coin(token: &MaybeToken) -> Result<&'static str, Box> { 32 | let coin = match token.token() { 33 | None => "solana", 34 | Some(token) => match token { 35 | Token::USDC => "usd-coin", 36 | Token::USDS => "usds", 37 | Token::USDT => "tether", 38 | Token::UXD => "uxd-stablecoin", 39 | Token::bSOL => "blazestake-staked-sol", 40 | Token::hSOL => "msol", 41 | Token::mSOL => "msol", 42 | Token::stSOL => "lido-staked-sol", 43 | Token::JitoSOL => "jito-staked-sol", 44 | Token::wSOL => "solana", 45 | Token::JLP => "jupiter-perpetuals-liquidity-provider-token", 46 | Token::JUP => "jupiter-exchange-solana", 47 | Token::JTO => "jito-governance-token", 48 | Token::BONK => "bonk", 49 | Token::KMNO => "kamino", 50 | Token::PYTH => "pyth-network", 51 | Token::WEN => "wen-4", 52 | Token::WIF => "dogwifcoin", 53 | Token::PYUSD => "paypal-usd", 54 | unsupported_token => { 55 | return Err(format!( 56 | "Coin Gecko price data not available for {}", 57 | unsupported_token.name() 58 | ) 59 | .into()) 60 | } 61 | }, 62 | }; 63 | Ok(coin) 64 | } 65 | 66 | fn get_cg_pro_api_key() -> (&'static str, String) { 67 | let (maybe_pro, x_cg_pro_api_key) = match env::var("CG_PRO_API_KEY") { 68 | Err(_) => ("", "".into()), 69 | Ok(x_cg_pro_api_key) => ("pro-", format!("&x_cg_pro_api_key={x_cg_pro_api_key}")), 70 | }; 71 | 72 | (maybe_pro, x_cg_pro_api_key) 73 | } 74 | 75 | pub async fn get_current_price(token: &MaybeToken) -> Result> { 76 | type CurrentPriceCache = HashMap; 77 | lazy_static::lazy_static! { 78 | static ref CURRENT_PRICE_CACHE: Arc> = Arc::new(RwLock::new(HashMap::new())); 79 | } 80 | let mut current_price_cache = CURRENT_PRICE_CACHE.write().await; 81 | 82 | match current_price_cache.get(token) { 83 | Some(price) => Ok(*price), 84 | None => { 85 | let coin = token_to_coin(token)?; 86 | 87 | let (maybe_pro, x_cg_pro_api_key) = get_cg_pro_api_key(); 88 | let url = format!( 89 | "https://{maybe_pro}api.coingecko.com/api/v3/simple/price?ids={coin}&vs_currencies=usd{x_cg_pro_api_key}" 90 | ); 91 | 92 | #[derive(Debug, Serialize, Deserialize)] 93 | struct Coins { 94 | solana: Option, 95 | #[serde(rename = "blazestake-staked-sol")] 96 | bsol: Option, 97 | #[serde(rename = "helius-staked-sol")] 98 | hsol: Option, 99 | msol: Option, 100 | #[serde(rename = "lido-staked-sol")] 101 | stsol: Option, 102 | #[serde(rename = "jito-staked-sol")] 103 | jitosol: Option, 104 | #[serde(rename = "tether")] 105 | tether: Option, 106 | #[serde(rename = "usds")] 107 | usds: Option, 108 | #[serde(rename = "uxd-stablecoin")] 109 | uxd: Option, 110 | #[serde(rename = "jupiter-perpetuals-liquidity-provider-token")] 111 | jlp: Option, 112 | #[serde(rename = "jupiter-exchange-solana")] 113 | jup: Option, 114 | #[serde(rename = "jito-governance-token")] 115 | jto: Option, 116 | #[serde(rename = "bonk")] 117 | bonk: Option, 118 | #[serde(rename = "kamino")] 119 | kmno: Option, 120 | #[serde(rename = "pyth-network")] 121 | pyth: Option, 122 | #[serde(rename = "wen-4")] 123 | wen: Option, 124 | #[serde(rename = "dogwifcoin")] 125 | wif: Option, 126 | #[serde(rename = "paypal-usd")] 127 | pyusd: Option, 128 | } 129 | 130 | let coins = reqwest::get(url).await?.json::().await?; 131 | 132 | coins 133 | .solana 134 | .or(coins.msol) 135 | .or(coins.stsol) 136 | .or(coins.jitosol) 137 | .or(coins.tether) 138 | .or(coins.usds) 139 | .or(coins.uxd) 140 | .or(coins.bsol) 141 | .or(coins.hsol) 142 | .or(coins.jlp) 143 | .or(coins.jup) 144 | .or(coins.jto) 145 | .or(coins.bonk) 146 | .or(coins.kmno) 147 | .or(coins.pyth) 148 | .or(coins.wen) 149 | .or(coins.wif) 150 | .or(coins.pyusd) 151 | .ok_or_else(|| format!("Simple price data not available for {coin}").into()) 152 | .map(|price| { 153 | let price = Decimal::from_f64(price.usd).unwrap(); 154 | current_price_cache.insert(*token, price); 155 | price 156 | }) 157 | } 158 | } 159 | } 160 | 161 | pub async fn get_historical_price( 162 | when: NaiveDate, 163 | token: &MaybeToken, 164 | ) -> Result> { 165 | type HistoricalPriceCache = HashMap<(NaiveDate, MaybeToken), Decimal>; 166 | lazy_static::lazy_static! { 167 | static ref HISTORICAL_PRICE_CACHE: Arc> = Arc::new(RwLock::new(HashMap::new())); 168 | } 169 | let mut historical_price_cache = HISTORICAL_PRICE_CACHE.write().await; 170 | 171 | let price_cache_key = (when, *token); 172 | 173 | match historical_price_cache.get(&price_cache_key) { 174 | Some(price) => Ok(*price), 175 | None => { 176 | let coin = token_to_coin(token)?; 177 | 178 | let (maybe_pro, x_cg_pro_api_key) = get_cg_pro_api_key(); 179 | let url = format!( 180 | "https://{maybe_pro}api.coingecko.com/api/v3/coins/{}/history?date={:02}-{:02}-{:4}&localization=false{x_cg_pro_api_key}", 181 | coin, 182 | when.day(), 183 | when.month(), 184 | when.year() 185 | ); 186 | 187 | reqwest::get(url) 188 | .await? 189 | .json::() 190 | .await? 191 | .market_data 192 | .ok_or_else(|| format!("Market data not available for {coin} on {when}").into()) 193 | .map(|market_data| { 194 | let price = Decimal::from_f64(market_data.current_price.usd).unwrap(); 195 | historical_price_cache.insert(price_cache_key, price); 196 | price 197 | }) 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/vendor/solend/state/rate_limiter.rs: -------------------------------------------------------------------------------- 1 | use crate::vendor::solend::state::{pack_decimal, unpack_decimal}; 2 | use solana_program::msg; 3 | use solana_program::program_pack::IsInitialized; 4 | use solana_program::{program_error::ProgramError, slot_history::Slot}; 5 | 6 | use crate::vendor::solend::{ 7 | error::LendingError, 8 | math::{Decimal, TryAdd, TryDiv, TryMul, TrySub}, 9 | }; 10 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 11 | use solana_program::program_pack::{Pack, Sealed}; 12 | 13 | /// Sliding Window Rate limiter 14 | /// guarantee: at any point, the outflow between [cur_slot - slot.window_duration, cur_slot] 15 | /// is less than 2x max_outflow. 16 | 17 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 18 | pub struct RateLimiter { 19 | /// configuration parameters 20 | pub config: RateLimiterConfig, 21 | 22 | // state 23 | /// prev qty is the sum of all outflows from [window_start - config.window_duration, window_start) 24 | prev_qty: Decimal, 25 | /// window_start is the start of the current window 26 | window_start: Slot, 27 | /// cur qty is the sum of all outflows from [window_start, window_start + config.window_duration) 28 | cur_qty: Decimal, 29 | } 30 | 31 | /// Lending market configuration parameters 32 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 33 | pub struct RateLimiterConfig { 34 | /// Rate limiter window size in slots 35 | pub window_duration: u64, 36 | /// Rate limiter param. Max outflow of tokens in a window 37 | pub max_outflow: u64, 38 | } 39 | 40 | impl RateLimiter { 41 | /// initialize rate limiter 42 | pub fn new(config: RateLimiterConfig, cur_slot: u64) -> Self { 43 | let slot_start = if config.window_duration != 0 { 44 | cur_slot / config.window_duration * config.window_duration 45 | } else { 46 | cur_slot 47 | }; 48 | 49 | Self { 50 | config, 51 | prev_qty: Decimal::zero(), 52 | window_start: slot_start, 53 | cur_qty: Decimal::zero(), 54 | } 55 | } 56 | 57 | fn _update(&mut self, cur_slot: u64) -> Result<(), ProgramError> { 58 | if cur_slot < self.window_start { 59 | msg!("Current slot is less than window start, which is impossible"); 60 | return Err(LendingError::InvalidAccountInput.into()); 61 | } 62 | 63 | // floor wrt window duration 64 | let cur_slot_start = cur_slot / self.config.window_duration * self.config.window_duration; 65 | 66 | // update prev window, current window 67 | match cur_slot_start.cmp(&(self.window_start + self.config.window_duration)) { 68 | // |<-prev window->|<-cur window (cur_slot is in here)->| 69 | std::cmp::Ordering::Less => (), 70 | 71 | // |<-prev window->|<-cur window->| (cur_slot is in here) | 72 | std::cmp::Ordering::Equal => { 73 | self.prev_qty = self.cur_qty; 74 | self.window_start = cur_slot_start; 75 | self.cur_qty = Decimal::zero(); 76 | } 77 | 78 | // |<-prev window->|<-cur window->|<-cur window + 1->| ... | (cur_slot is in here) | 79 | std::cmp::Ordering::Greater => { 80 | self.prev_qty = Decimal::zero(); 81 | self.window_start = cur_slot_start; 82 | self.cur_qty = Decimal::zero(); 83 | } 84 | }; 85 | 86 | Ok(()) 87 | } 88 | 89 | /// Calculate current outflow. Must only be called after ._update()! 90 | fn current_outflow(&self, cur_slot: u64) -> Result { 91 | if self.config.window_duration == 0 { 92 | msg!("Window duration cannot be 0"); 93 | return Err(LendingError::InvalidAccountInput.into()); 94 | } 95 | 96 | // assume the prev_window's outflow is even distributed across the window 97 | // this isn't true, but it's a good enough approximation 98 | let prev_weight = Decimal::from(self.config.window_duration) 99 | .try_sub(Decimal::from(cur_slot - self.window_start + 1))? 100 | .try_div(self.config.window_duration)?; 101 | 102 | prev_weight.try_mul(self.prev_qty)?.try_add(self.cur_qty) 103 | } 104 | 105 | /// Calculate remaining outflow for the current window 106 | pub fn remaining_outflow(&mut self, cur_slot: u64) -> Result { 107 | // rate limiter is disabled if window duration == 0. this is here because we don't want to 108 | // brick borrows/withdraws in permissionless pools on program upgrade. 109 | if self.config.window_duration == 0 { 110 | return Ok(Decimal::from(u64::MAX)); 111 | } 112 | 113 | self._update(cur_slot)?; 114 | 115 | let cur_outflow = self.current_outflow(cur_slot)?; 116 | if cur_outflow > Decimal::from(self.config.max_outflow) { 117 | return Ok(Decimal::zero()); 118 | } 119 | 120 | let diff = Decimal::from(self.config.max_outflow).try_sub(cur_outflow)?; 121 | Ok(diff) 122 | } 123 | 124 | /// update rate limiter with new quantity. errors if rate limit has been reached 125 | pub fn update(&mut self, cur_slot: u64, qty: Decimal) -> Result<(), ProgramError> { 126 | // rate limiter is disabled if window duration == 0. this is here because we don't want to 127 | // brick borrows/withdraws in permissionless pools on program upgrade. 128 | if self.config.window_duration == 0 { 129 | return Ok(()); 130 | } 131 | 132 | self._update(cur_slot)?; 133 | 134 | let cur_outflow = self.current_outflow(cur_slot)?; 135 | if cur_outflow.try_add(qty)? > Decimal::from(self.config.max_outflow) { 136 | Err(LendingError::OutflowRateLimitExceeded.into()) 137 | } else { 138 | self.cur_qty = self.cur_qty.try_add(qty)?; 139 | Ok(()) 140 | } 141 | } 142 | } 143 | 144 | impl Default for RateLimiter { 145 | fn default() -> Self { 146 | Self::new( 147 | RateLimiterConfig { 148 | window_duration: 1, 149 | max_outflow: u64::MAX, 150 | }, 151 | 1, 152 | ) 153 | } 154 | } 155 | 156 | impl Sealed for RateLimiter {} 157 | 158 | impl IsInitialized for RateLimiter { 159 | fn is_initialized(&self) -> bool { 160 | true 161 | } 162 | } 163 | 164 | /// Size of RateLimiter when packed into account 165 | pub const RATE_LIMITER_LEN: usize = 56; 166 | impl Pack for RateLimiter { 167 | const LEN: usize = RATE_LIMITER_LEN; 168 | 169 | fn pack_into_slice(&self, dst: &mut [u8]) { 170 | let dst = array_mut_ref![dst, 0, RATE_LIMITER_LEN]; 171 | let ( 172 | config_max_outflow_dst, 173 | config_window_duration_dst, 174 | prev_qty_dst, 175 | window_start_dst, 176 | cur_qty_dst, 177 | ) = mut_array_refs![dst, 8, 8, 16, 8, 16]; 178 | *config_max_outflow_dst = self.config.max_outflow.to_le_bytes(); 179 | *config_window_duration_dst = self.config.window_duration.to_le_bytes(); 180 | pack_decimal(self.prev_qty, prev_qty_dst); 181 | *window_start_dst = self.window_start.to_le_bytes(); 182 | pack_decimal(self.cur_qty, cur_qty_dst); 183 | } 184 | 185 | fn unpack_from_slice(src: &[u8]) -> Result { 186 | let src = array_ref![src, 0, RATE_LIMITER_LEN]; 187 | let ( 188 | config_max_outflow_src, 189 | config_window_duration_src, 190 | prev_qty_src, 191 | window_start_src, 192 | cur_qty_src, 193 | ) = array_refs![src, 8, 8, 16, 8, 16]; 194 | 195 | Ok(Self { 196 | config: RateLimiterConfig { 197 | max_outflow: u64::from_le_bytes(*config_max_outflow_src), 198 | window_duration: u64::from_le_bytes(*config_window_duration_src), 199 | }, 200 | prev_qty: unpack_decimal(prev_qty_src), 201 | window_start: u64::from_le_bytes(*window_start_src), 202 | cur_qty: unpack_decimal(cur_qty_src), 203 | }) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/vendor/kamino/fraction.rs: -------------------------------------------------------------------------------- 1 | use fixed::traits::{FromFixed, ToFixed}; 2 | pub use fixed::types::U68F60 as Fraction; 3 | 4 | pub enum LendingError { 5 | IntegerOverflow, 6 | } 7 | 8 | #[allow(clippy::assign_op_pattern)] 9 | #[allow(clippy::reversed_empty_ranges)] 10 | #[allow(clippy::manual_div_ceil)] 11 | mod uint_types { 12 | use uint::construct_uint; 13 | construct_uint! { 14 | pub struct U256(4); 15 | } 16 | construct_uint! { 17 | pub struct U128(2); 18 | } 19 | } 20 | 21 | pub use uint_types::{U128, U256}; 22 | 23 | /* 24 | pub fn pow_fraction(fraction: Fraction, power: u32) -> Option { 25 | if power == 0 { 26 | return Some(Fraction::ONE); 27 | } 28 | 29 | let mut x = fraction; 30 | let mut y = Fraction::ONE; 31 | let mut n = power; 32 | 33 | while n > 1 { 34 | if n % 2 == 1 { 35 | y = x.checked_mul(y)?; 36 | } 37 | x = x.checked_mul(x)?; 38 | n /= 2; 39 | } 40 | 41 | x.checked_mul(y) 42 | } 43 | */ 44 | 45 | pub trait FractionExtra { 46 | fn to_bps(&self) -> Option; 47 | fn from_bps(bps: Src) -> Self; 48 | fn to_floor(&self) -> Dst; 49 | /* 50 | fn to_percent(&self) -> Option; 51 | fn from_percent(percent: Src) -> Self; 52 | fn checked_pow(&self, power: u32) -> Option 53 | where 54 | Self: std::marker::Sized; 55 | 56 | fn to_ceil(&self) -> Dst; 57 | fn to_round(&self) -> Dst; 58 | 59 | fn to_sf(&self) -> u128; 60 | fn from_sf(sf: u128) -> Self; 61 | 62 | fn to_display(&self) -> FractionDisplay; 63 | */ 64 | } 65 | 66 | impl FractionExtra for Fraction { 67 | #[inline] 68 | fn to_bps(&self) -> Option { 69 | (self * 10_000).round().checked_to_num() 70 | } 71 | 72 | #[inline] 73 | fn from_bps(bps: Src) -> Self { 74 | let bps = Fraction::from_num(bps); 75 | bps / 10_000 76 | } 77 | 78 | #[inline] 79 | fn to_floor(&self) -> Dst { 80 | self.floor().to_num() 81 | } 82 | 83 | /* 84 | #[inline] 85 | fn to_percent(&self) -> Option { 86 | (self * 100).round().checked_to_num() 87 | } 88 | 89 | #[inline] 90 | fn from_percent(percent: Src) -> Self { 91 | let percent = Fraction::from_num(percent); 92 | percent / 100 93 | } 94 | 95 | #[inline] 96 | fn checked_pow(&self, power: u32) -> Option 97 | where 98 | Self: std::marker::Sized, 99 | { 100 | pow_fraction(*self, power) 101 | } 102 | 103 | #[inline] 104 | fn to_ceil(&self) -> Dst { 105 | self.ceil().to_num() 106 | } 107 | 108 | #[inline] 109 | fn to_round(&self) -> Dst { 110 | self.round().to_num() 111 | } 112 | 113 | #[inline] 114 | fn to_sf(&self) -> u128 { 115 | self.to_bits() 116 | } 117 | 118 | #[inline] 119 | fn from_sf(sf: u128) -> Self { 120 | Fraction::from_bits(sf) 121 | } 122 | 123 | #[inline] 124 | fn to_display(&self) -> FractionDisplay { 125 | FractionDisplay(self) 126 | } 127 | */ 128 | } 129 | 130 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PartialOrd, Ord)] 131 | pub struct BigFraction(pub U256); 132 | 133 | impl From for BigFraction 134 | where 135 | T: Into, 136 | { 137 | fn from(fraction: T) -> Self { 138 | let fraction: Fraction = fraction.into(); 139 | let repr_fraction = fraction.to_bits(); 140 | Self(U256::from(repr_fraction)) 141 | } 142 | } 143 | 144 | impl TryFrom for Fraction { 145 | type Error = LendingError; 146 | 147 | fn try_from(value: BigFraction) -> Result { 148 | let repr_faction: u128 = value 149 | .0 150 | .try_into() 151 | .map_err(|_| LendingError::IntegerOverflow)?; 152 | Ok(Fraction::from_bits(repr_faction)) 153 | } 154 | } 155 | 156 | impl BigFraction { 157 | pub fn from_bits(bits: [u64; 4]) -> Self { 158 | Self(U256(bits)) 159 | } 160 | 161 | pub fn from_num(num: T) -> Self 162 | where 163 | T: Into, 164 | { 165 | let value: U256 = num.into(); 166 | let sf = value << Fraction::FRAC_NBITS; 167 | Self(sf) 168 | } 169 | } 170 | 171 | use std::{ 172 | fmt::Display, 173 | ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}, 174 | }; 175 | 176 | impl Add for BigFraction { 177 | type Output = Self; 178 | 179 | fn add(self, rhs: Self) -> Self::Output { 180 | Self(self.0 + rhs.0) 181 | } 182 | } 183 | 184 | impl AddAssign for BigFraction { 185 | fn add_assign(&mut self, rhs: Self) { 186 | self.0 += rhs.0; 187 | } 188 | } 189 | 190 | impl Sub for BigFraction { 191 | type Output = Self; 192 | 193 | fn sub(self, rhs: Self) -> Self::Output { 194 | Self(self.0 - rhs.0) 195 | } 196 | } 197 | 198 | impl SubAssign for BigFraction { 199 | fn sub_assign(&mut self, rhs: Self) { 200 | self.0 -= rhs.0; 201 | } 202 | } 203 | 204 | impl Mul for BigFraction { 205 | type Output = Self; 206 | 207 | fn mul(self, rhs: Self) -> Self::Output { 208 | let extra_scaled = self.0 * rhs.0; 209 | let res = extra_scaled >> Fraction::FRAC_NBITS; 210 | Self(res) 211 | } 212 | } 213 | 214 | impl MulAssign for BigFraction { 215 | fn mul_assign(&mut self, rhs: Self) { 216 | *self = *self * rhs; 217 | } 218 | } 219 | 220 | impl Div for BigFraction { 221 | type Output = Self; 222 | 223 | fn div(self, rhs: Self) -> Self::Output { 224 | let extra_scaled = self.0 << Fraction::FRAC_NBITS; 225 | let res = extra_scaled / rhs.0; 226 | Self(res) 227 | } 228 | } 229 | 230 | impl DivAssign for BigFraction { 231 | fn div_assign(&mut self, rhs: Self) { 232 | *self = *self / rhs; 233 | } 234 | } 235 | 236 | impl Mul for BigFraction 237 | where 238 | T: Into, 239 | { 240 | type Output = Self; 241 | 242 | fn mul(self, rhs: T) -> Self::Output { 243 | let rhs: U256 = rhs.into(); 244 | Self(self.0 * rhs) 245 | } 246 | } 247 | 248 | impl MulAssign for BigFraction 249 | where 250 | T: Into, 251 | { 252 | fn mul_assign(&mut self, rhs: T) { 253 | let rhs: U256 = rhs.into(); 254 | self.0 *= rhs; 255 | } 256 | } 257 | 258 | impl Div for BigFraction 259 | where 260 | T: Into, 261 | { 262 | type Output = Self; 263 | 264 | fn div(self, rhs: T) -> Self::Output { 265 | let rhs: U256 = rhs.into(); 266 | Self(self.0 / rhs) 267 | } 268 | } 269 | 270 | impl DivAssign for BigFraction 271 | where 272 | T: Into, 273 | { 274 | fn div_assign(&mut self, rhs: T) { 275 | let rhs: U256 = rhs.into(); 276 | self.0 /= rhs; 277 | } 278 | } 279 | 280 | impl From for U256 { 281 | fn from(value: U128) -> Self { 282 | Self([value.0[0], value.0[1], 0, 0]) 283 | } 284 | } 285 | 286 | impl TryFrom for U128 { 287 | type Error = LendingError; 288 | 289 | fn try_from(value: U256) -> Result { 290 | if value.0[2] != 0 || value.0[3] != 0 { 291 | return Err(LendingError::IntegerOverflow); 292 | } 293 | Ok(Self([value.0[0], value.0[1]])) 294 | } 295 | } 296 | 297 | #[allow(dead_code)] 298 | pub struct FractionDisplay<'a>(&'a Fraction); 299 | 300 | impl Display for FractionDisplay<'_> { 301 | fn fmt(&self, formater: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 302 | let sf = self.0.to_bits(); 303 | 304 | const ROUND_COMP: u128 = (1 << Fraction::FRAC_NBITS) / (10_000 * 2); 305 | let sf = sf + ROUND_COMP; 306 | 307 | let i = sf >> Fraction::FRAC_NBITS; 308 | 309 | const FRAC_MASK: u128 = (1 << Fraction::FRAC_NBITS) - 1; 310 | let f_p = (sf & FRAC_MASK) as u64; 311 | let f_p = ((f_p >> 30) * 10_000) >> 30; 312 | write!(formater, "{i}.{f_p:0>4}") 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/vendor/solend/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use num_derive::FromPrimitive; 4 | use num_traits::FromPrimitive; 5 | use solana_program::{decode_error::DecodeError, program_error::ProgramError}; 6 | use solana_program::{msg, program_error::PrintProgramError}; 7 | use thiserror::Error; 8 | 9 | /// Errors that may be returned by the TokenLending program. 10 | #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] 11 | pub enum LendingError { 12 | // 0 13 | /// Invalid instruction data passed in. 14 | #[error("Failed to unpack instruction data")] 15 | InstructionUnpackError, 16 | /// The account cannot be initialized because it is already in use. 17 | #[error("Account is already initialized")] 18 | AlreadyInitialized, 19 | /// Lamport balance below rent-exempt threshold. 20 | #[error("Lamport balance below rent-exempt threshold")] 21 | NotRentExempt, 22 | /// The program address provided doesn't match the value generated by the program. 23 | #[error("Market authority is invalid")] 24 | InvalidMarketAuthority, 25 | /// Expected a different market owner 26 | #[error("Market owner is invalid")] 27 | InvalidMarketOwner, 28 | 29 | // 5 30 | /// The owner of the input isn't set to the program address generated by the program. 31 | #[error("Input account owner is not the program address")] 32 | InvalidAccountOwner, 33 | /// The owner of the account input isn't set to the correct token program id. 34 | #[error("Input token account is not owned by the correct token program id")] 35 | InvalidTokenOwner, 36 | /// Expected an SPL Token account 37 | #[error("Input token account is not valid")] 38 | InvalidTokenAccount, 39 | /// Expected an SPL Token mint 40 | #[error("Input token mint account is not valid")] 41 | InvalidTokenMint, 42 | /// Expected a different SPL Token program 43 | #[error("Input token program account is not valid")] 44 | InvalidTokenProgram, 45 | 46 | // 10 47 | /// Invalid amount, must be greater than zero 48 | #[error("Input amount is invalid")] 49 | InvalidAmount, 50 | /// Invalid config value 51 | #[error("Input config value is invalid")] 52 | InvalidConfig, 53 | /// Invalid config value 54 | #[error("Input account must be a signer")] 55 | InvalidSigner, 56 | /// Invalid account input 57 | #[error("Invalid account input")] 58 | InvalidAccountInput, 59 | /// Math operation overflow 60 | #[error("Math operation overflow")] 61 | MathOverflow, 62 | 63 | // 15 64 | /// Token initialize mint failed 65 | #[error("Token initialize mint failed")] 66 | TokenInitializeMintFailed, 67 | /// Token initialize account failed 68 | #[error("Token initialize account failed")] 69 | TokenInitializeAccountFailed, 70 | /// Token transfer failed 71 | #[error("Token transfer failed")] 72 | TokenTransferFailed, 73 | /// Token mint to failed 74 | #[error("Token mint to failed")] 75 | TokenMintToFailed, 76 | /// Token burn failed 77 | #[error("Token burn failed")] 78 | TokenBurnFailed, 79 | 80 | // 20 81 | /// Insufficient liquidity available 82 | #[error("Insufficient liquidity available")] 83 | InsufficientLiquidity, 84 | /// This reserve's collateral cannot be used for borrows 85 | #[error("Input reserve has collateral disabled")] 86 | ReserveCollateralDisabled, 87 | /// Reserve state stale 88 | #[error("Reserve state needs to be refreshed")] 89 | ReserveStale, 90 | /// Withdraw amount too small 91 | #[error("Withdraw amount too small")] 92 | WithdrawTooSmall, 93 | /// Withdraw amount too large 94 | #[error("Withdraw amount too large")] 95 | WithdrawTooLarge, 96 | 97 | // 25 98 | /// Borrow amount too small 99 | #[error("Borrow amount too small to receive liquidity after fees")] 100 | BorrowTooSmall, 101 | /// Borrow amount too large 102 | #[error("Borrow amount too large for deposited collateral")] 103 | BorrowTooLarge, 104 | /// Repay amount too small 105 | #[error("Repay amount too small to transfer liquidity")] 106 | RepayTooSmall, 107 | /// Liquidation amount too small 108 | #[error("Liquidation amount too small to receive collateral")] 109 | LiquidationTooSmall, 110 | /// Cannot liquidate healthy obligations 111 | #[error("Cannot liquidate healthy obligations")] 112 | ObligationHealthy, 113 | 114 | // 30 115 | /// Obligation state stale 116 | #[error("Obligation state needs to be refreshed")] 117 | ObligationStale, 118 | /// Obligation reserve limit exceeded 119 | #[error("Obligation reserve limit exceeded")] 120 | ObligationReserveLimit, 121 | /// Expected a different obligation owner 122 | #[error("Obligation owner is invalid")] 123 | InvalidObligationOwner, 124 | /// Obligation deposits are empty 125 | #[error("Obligation deposits are empty")] 126 | ObligationDepositsEmpty, 127 | /// Obligation borrows are empty 128 | #[error("Obligation borrows are empty")] 129 | ObligationBorrowsEmpty, 130 | 131 | // 35 132 | /// Obligation deposits have zero value 133 | #[error("Obligation deposits have zero value")] 134 | ObligationDepositsZero, 135 | /// Obligation borrows have zero value 136 | #[error("Obligation borrows have zero value")] 137 | ObligationBorrowsZero, 138 | /// Invalid obligation collateral 139 | #[error("Invalid obligation collateral")] 140 | InvalidObligationCollateral, 141 | /// Invalid obligation liquidity 142 | #[error("Invalid obligation liquidity")] 143 | InvalidObligationLiquidity, 144 | /// Obligation collateral is empty 145 | #[error("Obligation collateral is empty")] 146 | ObligationCollateralEmpty, 147 | 148 | // 40 149 | /// Obligation liquidity is empty 150 | #[error("Obligation liquidity is empty")] 151 | ObligationLiquidityEmpty, 152 | /// Negative interest rate 153 | #[error("Interest rate is negative")] 154 | NegativeInterestRate, 155 | /// Oracle config is invalid 156 | #[error("Input oracle config is invalid")] 157 | InvalidOracleConfig, 158 | /// Expected a different flash loan receiver program 159 | #[error("Input flash loan receiver program account is not valid")] 160 | InvalidFlashLoanReceiverProgram, 161 | /// Not enough liquidity after flash loan 162 | #[error("Not enough liquidity after flash loan")] 163 | NotEnoughLiquidityAfterFlashLoan, 164 | 165 | // 45 166 | /// Null oracle config 167 | #[error("Null oracle config")] 168 | NullOracleConfig, 169 | /// Insufficent protocol fees to redeem or no liquidity availible to process redeem 170 | #[error("Insufficent protocol fees to claim or no liquidity availible")] 171 | InsufficientProtocolFeesToRedeem, 172 | /// No cpi flash borrows allowed 173 | #[error("No cpi flash borrows allowed")] 174 | FlashBorrowCpi, 175 | /// No corresponding repay found for flash borrow 176 | #[error("No corresponding repay found for flash borrow")] 177 | NoFlashRepayFound, 178 | /// Invalid flash repay found for borrow 179 | #[error("Invalid repay found")] 180 | InvalidFlashRepay, 181 | 182 | // 50 183 | /// No cpi flash repays allowed 184 | #[error("No cpi flash repays allowed")] 185 | FlashRepayCpi, 186 | /// Multiple flash borrows not allowed in the same transaction 187 | #[error("Multiple flash borrows not allowed in the same transaction")] 188 | MultipleFlashBorrows, 189 | /// Flash loans are disabled for this reserve 190 | #[error("Flash loans are disabled for this reserve")] 191 | FlashLoansDisabled, 192 | /// Deprecated instruction 193 | #[error("Instruction is deprecated")] 194 | DeprecatedInstruction, 195 | /// Outflow Rate Limit Exceeded 196 | #[error("Outflow Rate Limit Exceeded")] 197 | OutflowRateLimitExceeded, 198 | 199 | // 55 200 | /// Not a whitelisted liquidator 201 | #[error("Not a whitelisted liquidator")] 202 | NotWhitelistedLiquidator, 203 | /// Isolated Tier Asset Violation 204 | #[error("Isolated Tier Asset Violation")] 205 | IsolatedTierAssetViolation, 206 | } 207 | 208 | impl From for ProgramError { 209 | fn from(e: LendingError) -> Self { 210 | ProgramError::Custom(e as u32) 211 | } 212 | } 213 | 214 | impl DecodeError for LendingError { 215 | fn type_of() -> &'static str { 216 | "Lending Error" 217 | } 218 | } 219 | 220 | impl PrintProgramError for LendingError { 221 | fn print(&self) 222 | where 223 | E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, 224 | { 225 | msg!(&self.to_string()); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/vendor/drift/user.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey::Pubkey; 2 | 3 | pub const SIZE: usize = 4376; 4 | 5 | #[derive(Eq, PartialEq, Debug)] 6 | #[repr(C)] 7 | pub struct User { 8 | /// The owner/authority of the account 9 | pub authority: Pubkey, 10 | /// An addresses that can control the account on the authority's behalf. Has limited power, cant withdraw 11 | pub delegate: Pubkey, 12 | /// Encoded display name e.g. "toly" 13 | pub name: [u8; 32], 14 | /// The user's spot positions 15 | pub spot_positions: [SpotPosition; 8], 16 | /// The user's perp positions 17 | pub perp_positions: [PerpPosition; 8], 18 | /// The user's orders 19 | pub orders: [Order; 32], 20 | /// The last time the user added perp lp positions 21 | pub last_add_perp_lp_shares_ts: i64, 22 | /// The total values of deposits the user has made 23 | /// precision: QUOTE_PRECISION 24 | pub total_deposits: u64, 25 | /// The total values of withdrawals the user has made 26 | /// precision: QUOTE_PRECISION 27 | pub total_withdraws: u64, 28 | /// The total socialized loss the users has incurred upon the protocol 29 | /// precision: QUOTE_PRECISION 30 | pub total_social_loss: u64, 31 | /// Fees (taker fees, maker rebate, referrer reward, filler reward) and pnl for perps 32 | /// precision: QUOTE_PRECISION 33 | pub settled_perp_pnl: i64, 34 | /// Fees (taker fees, maker rebate, filler reward) for spot 35 | /// precision: QUOTE_PRECISION 36 | pub cumulative_spot_fees: i64, 37 | /// Cumulative funding paid/received for perps 38 | /// precision: QUOTE_PRECISION 39 | pub cumulative_perp_funding: i64, 40 | /// The amount of margin freed during liquidation. Used to force the liquidation to occur over a period of time 41 | /// Defaults to zero when not being liquidated 42 | /// precision: QUOTE_PRECISION 43 | pub liquidation_margin_freed: u64, 44 | /// The last slot a user was active. Used to determine if a user is idle 45 | pub last_active_slot: u64, 46 | /// Every user order has an order id. This is the next order id to be used 47 | pub next_order_id: u32, 48 | /// Custom max initial margin ratio for the user 49 | pub max_margin_ratio: u32, 50 | /// The next liquidation id to be used for user 51 | pub next_liquidation_id: u16, 52 | /// The sub account id for this user 53 | pub sub_account_id: u16, 54 | /// Whether the user is active, being liquidated or bankrupt 55 | pub status: u8, 56 | /// Whether the user has enabled margin trading 57 | pub is_margin_trading_enabled: bool, 58 | /// User is idle if they haven't interacted with the protocol in 1 week and they have no orders, perp positions or borrows 59 | /// Off-chain keeper bots can ignore users that are idle 60 | pub idle: bool, 61 | /// number of open orders 62 | pub open_orders: u8, 63 | /// Whether or not user has open order 64 | pub has_open_order: bool, 65 | /// number of open orders with auction 66 | pub open_auctions: u8, 67 | /// Whether or not user has open order with auction 68 | pub has_open_auction: bool, 69 | pub padding: [u8; 21], 70 | } 71 | 72 | #[derive(Eq, PartialEq, Debug)] 73 | #[repr(C)] 74 | pub struct SpotPosition { 75 | /// The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow 76 | /// interest of corresponding market. 77 | /// precision: SPOT_BALANCE_PRECISION 78 | pub scaled_balance: u64, 79 | /// How many spot bids the user has open 80 | /// precision: token mint precision 81 | pub open_bids: i64, 82 | /// How many spot asks the user has open 83 | /// precision: token mint precision 84 | pub open_asks: i64, 85 | /// The cumulative deposits/borrows a user has made into a market 86 | /// precision: token mint precision 87 | pub cumulative_deposits: i64, 88 | /// The market index of the corresponding spot market 89 | pub market_index: u16, 90 | /// Whether the position is deposit or borrow 91 | pub balance_type: SpotBalanceType, 92 | /// Number of open orders 93 | pub open_orders: u8, 94 | pub padding: [u8; 4], 95 | } 96 | 97 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 98 | pub enum SpotBalanceType { 99 | Deposit, 100 | Borrow, 101 | } 102 | 103 | #[derive(Debug, Eq, PartialEq)] 104 | #[repr(C)] 105 | pub struct PerpPosition { 106 | /// The perp market's last cumulative funding rate. Used to calculate the funding payment owed to user 107 | /// precision: FUNDING_RATE_PRECISION 108 | pub last_cumulative_funding_rate: i64, 109 | /// the size of the users perp position 110 | /// precision: BASE_PRECISION 111 | pub base_asset_amount: i64, 112 | /// Used to calculate the users pnl. Upon entry, is equal to base_asset_amount * avg entry price - fees 113 | /// Updated when the user open/closes position or settles pnl. Includes fees/funding 114 | /// precision: QUOTE_PRECISION 115 | pub quote_asset_amount: i64, 116 | /// The amount of quote the user would need to exit their position at to break even 117 | /// Updated when the user open/closes position or settles pnl. Includes fees/funding 118 | /// precision: QUOTE_PRECISION 119 | pub quote_break_even_amount: i64, 120 | /// The amount quote the user entered the position with. Equal to base asset amount * avg entry price 121 | /// Updated when the user open/closes position. Excludes fees/funding 122 | /// precision: QUOTE_PRECISION 123 | pub quote_entry_amount: i64, 124 | /// The amount of open bids the user has in this perp market 125 | /// precision: BASE_PRECISION 126 | pub open_bids: i64, 127 | /// The amount of open asks the user has in this perp market 128 | /// precision: BASE_PRECISION 129 | pub open_asks: i64, 130 | /// The amount of pnl settled in this market since opening the position 131 | /// precision: QUOTE_PRECISION 132 | pub settled_pnl: i64, 133 | /// The number of lp (liquidity provider) shares the user has in this perp market 134 | /// LP shares allow users to provide liquidity via the AMM 135 | /// precision: BASE_PRECISION 136 | pub lp_shares: u64, 137 | /// The last base asset amount per lp the amm had 138 | /// Used to settle the users lp position 139 | /// precision: BASE_PRECISION 140 | pub last_base_asset_amount_per_lp: i64, 141 | /// The last quote asset amount per lp the amm had 142 | /// Used to settle the users lp position 143 | /// precision: QUOTE_PRECISION 144 | pub last_quote_asset_amount_per_lp: i64, 145 | /// Settling LP position can lead to a small amount of base asset being left over smaller than step size 146 | /// This records that remainder so it can be settled later on 147 | /// precision: BASE_PRECISION 148 | pub remainder_base_asset_amount: i32, 149 | /// The market index for the perp market 150 | pub market_index: u16, 151 | /// The number of open orders 152 | pub open_orders: u8, 153 | pub per_lp_base: i8, 154 | } 155 | 156 | #[derive(Eq, PartialEq, Debug)] 157 | #[repr(C)] 158 | pub struct Order { 159 | pub slot: u64, 160 | pub price: u64, 161 | pub base_asset_amount: u64, 162 | pub base_asset_amount_filled: u64, 163 | pub quote_asset_amount_filled: u64, 164 | pub trigger_price: u64, 165 | pub auction_start_price: i64, 166 | pub auction_end_price: i64, 167 | pub max_ts: i64, 168 | pub oracle_price_offset: i32, 169 | pub order_id: u32, 170 | pub market_index: u16, 171 | pub status: OrderStatus, 172 | pub order_type: OrderType, 173 | pub market_type: MarketType, 174 | pub user_order_id: u8, 175 | pub existing_position_direction: PositionDirection, 176 | pub direction: PositionDirection, 177 | pub reduce_only: bool, 178 | pub post_only: bool, 179 | pub immediate_or_cancel: bool, 180 | pub trigger_condition: OrderTriggerCondition, 181 | pub auction_duration: u8, 182 | pub padding: [u8; 3], 183 | } 184 | 185 | #[derive(Clone, Copy, PartialEq, Debug, Eq)] 186 | pub enum OrderStatus { 187 | Init, 188 | Open, 189 | Filled, 190 | Canceled, 191 | } 192 | 193 | #[derive(Clone, Copy, PartialEq, Debug, Eq)] 194 | pub enum OrderType { 195 | Market, 196 | Limit, 197 | TriggerMarket, 198 | TriggerLimit, 199 | Oracle, 200 | } 201 | 202 | #[derive(Clone, Copy, PartialEq, Debug, Eq)] 203 | pub enum OrderTriggerCondition { 204 | Above, 205 | Below, 206 | TriggeredAbove, // above condition has been triggered 207 | TriggeredBelow, // below condition has been triggered 208 | } 209 | 210 | #[derive(Clone, Copy, PartialEq, Debug, Eq)] 211 | pub enum MarketType { 212 | Spot, 213 | Perp, 214 | } 215 | 216 | #[derive(Clone, Copy, PartialEq, Debug, Eq)] 217 | pub enum PositionDirection { 218 | Long, 219 | Short, 220 | } 221 | -------------------------------------------------------------------------------- /src/priority_fee.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{helius_rpc, RpcClients}, 3 | solana_client::rpc_client::RpcClient, 4 | solana_sdk::{ 5 | compute_budget, 6 | instruction::Instruction, 7 | native_token::lamports_to_sol, 8 | native_token::{sol_to_lamports, Sol}, 9 | }, 10 | }; 11 | 12 | #[derive(Debug, Clone, Copy)] 13 | pub enum PriorityFee { 14 | Auto { 15 | max_lamports: u64, 16 | fee_percentile: u8, 17 | }, 18 | Exact { 19 | lamports: u64, 20 | }, 21 | } 22 | 23 | impl PriorityFee { 24 | pub fn default_auto() -> Self { 25 | Self::default_auto_percentile(sol_to_lamports(0.005)) // Same max as the Jupiter V6 Swap API 26 | } 27 | pub fn default_auto_percentile(max_lamports: u64) -> Self { 28 | Self::Auto { 29 | max_lamports, 30 | fee_percentile: 90, // Pay at this percentile of recent fees 31 | } 32 | } 33 | } 34 | 35 | impl PriorityFee { 36 | pub fn max_lamports(&self) -> u64 { 37 | match self { 38 | Self::Auto { max_lamports, .. } => *max_lamports, 39 | Self::Exact { lamports } => *lamports, 40 | } 41 | } 42 | 43 | pub fn exact_lamports(&self) -> Option { 44 | match self { 45 | Self::Auto { .. } => None, 46 | Self::Exact { lamports } => Some(*lamports), 47 | } 48 | } 49 | } 50 | 51 | #[derive(Default, Debug, Clone, Copy)] 52 | pub struct ComputeBudget { 53 | pub compute_unit_price_micro_lamports: u64, 54 | pub compute_unit_limit: u32, 55 | } 56 | 57 | impl ComputeBudget { 58 | pub fn new(compute_unit_limit: u32, priority_fee_lamports: u64) -> Self { 59 | assert_ne!(compute_unit_limit, 0); 60 | Self { 61 | compute_unit_price_micro_lamports: priority_fee_lamports * (1e6 as u64) 62 | / compute_unit_limit as u64, 63 | compute_unit_limit, 64 | } 65 | } 66 | 67 | pub fn priority_fee_lamports(&self) -> u64 { 68 | self.compute_unit_limit as u64 * self.compute_unit_price_micro_lamports / (1e6 as u64) 69 | } 70 | } 71 | 72 | // Returns a sorted list of compute unit prices in micro lamports, from low to high 73 | fn get_recent_priority_fees_for_instructions( 74 | rpc_client: &RpcClient, 75 | instructions: &[Instruction], 76 | ) -> Result, String> { 77 | let mut account_keys: Vec<_> = instructions 78 | .iter() 79 | .flat_map(|instruction| { 80 | instruction 81 | .accounts 82 | .iter() 83 | .filter_map(|account_meta| account_meta.is_writable.then_some(account_meta.pubkey)) 84 | .collect::>() 85 | }) 86 | .collect(); 87 | account_keys.sort(); 88 | account_keys.dedup(); 89 | 90 | let prioritization_fees: Vec<_> = rpc_client 91 | .get_recent_prioritization_fees(&account_keys) 92 | .map(|response| { 93 | response 94 | .into_iter() 95 | .map(|rpf| rpf.prioritization_fee) 96 | .collect() 97 | }) 98 | .map_err(|err| format!("Failed to invoke RPC method getRecentPrioritizationFees: {err}"))?; 99 | 100 | Ok(prioritization_fees) 101 | } 102 | 103 | pub fn apply_priority_fee( 104 | rpc_clients: &RpcClients, 105 | instructions: &mut Vec, 106 | compute_unit_limit: u32, 107 | priority_fee: PriorityFee, 108 | ) -> Result> { 109 | assert_ne!(compute_unit_limit, 0); 110 | 111 | let compute_budget = match priority_fee { 112 | PriorityFee::Exact { lamports } => ComputeBudget::new(compute_unit_limit, lamports), 113 | PriorityFee::Auto { 114 | max_lamports, 115 | fee_percentile, 116 | } => { 117 | let helius_compute_budget = if let Ok(helius_priority_fee_estimate) = 118 | helius_rpc::get_priority_fee_estimate_for_instructions( 119 | rpc_clients.helius_or_default(), 120 | helius_rpc::HeliusPriorityLevel::High, 121 | instructions, 122 | ) { 123 | let helius_compute_budget = ComputeBudget { 124 | compute_unit_price_micro_lamports: helius_priority_fee_estimate, 125 | compute_unit_limit, 126 | }; 127 | 128 | println!( 129 | "Helius priority fee (high): {}", 130 | Sol(helius_compute_budget.priority_fee_lamports()) 131 | ); 132 | 133 | helius_compute_budget 134 | } else { 135 | ComputeBudget::new(compute_unit_limit, 0) 136 | }; 137 | 138 | let sys_compute_budget = { 139 | let recent_compute_unit_prices = 140 | get_recent_priority_fees_for_instructions(rpc_clients.default(), instructions)? 141 | .into_iter() 142 | // .skip_while(|fee| *fee == 0) // Skip 0 fee payers 143 | .map(|f| f as f64) 144 | .collect::>(); 145 | 146 | let ui_fee_for = |compute_unit_price_micro_lamports: f64| { 147 | Sol(ComputeBudget { 148 | compute_unit_price_micro_lamports: compute_unit_price_micro_lamports as u64, 149 | compute_unit_limit, 150 | } 151 | .priority_fee_lamports()) 152 | }; 153 | 154 | let dist = criterion_stats::Distribution::from( 155 | recent_compute_unit_prices.into_boxed_slice(), 156 | ); 157 | let mut verbose_msg = format!("mean={}", ui_fee_for(dist.mean())); 158 | let percentiles = dist.percentiles(); 159 | for i in [50., 75., 90., 95., 100.] { 160 | verbose_msg += &format!(", {i}th={}", ui_fee_for(percentiles.at(i))); 161 | } 162 | 163 | let fee_percentile_compute_unit_price_micro_lamports = 164 | percentiles.at(fee_percentile as f64) as u64; 165 | let mean_compute_unit_price_micro_lamports = dist.mean() as u64; 166 | 167 | // Use the greater of the `fee_percentile`th percentile fee or the mean fee 168 | let compute_unit_price_micro_lamports = 169 | if fee_percentile_compute_unit_price_micro_lamports 170 | > mean_compute_unit_price_micro_lamports 171 | { 172 | verbose_msg += &format!(". Selected {fee_percentile}th percentile"); 173 | fee_percentile_compute_unit_price_micro_lamports 174 | } else { 175 | verbose_msg += ". Selected mean"; 176 | mean_compute_unit_price_micro_lamports 177 | }; 178 | 179 | let sys_compute_budget = ComputeBudget { 180 | compute_unit_price_micro_lamports, 181 | compute_unit_limit, 182 | }; 183 | 184 | println!( 185 | "Observed priority fee: {}\n ({verbose_msg})", 186 | Sol(sys_compute_budget.priority_fee_lamports()) 187 | ); 188 | sys_compute_budget 189 | }; 190 | 191 | let compute_budget = if sys_compute_budget.compute_unit_price_micro_lamports 192 | > helius_compute_budget.compute_unit_price_micro_lamports 193 | { 194 | sys_compute_budget 195 | } else { 196 | helius_compute_budget 197 | }; 198 | 199 | if compute_budget.priority_fee_lamports() > max_lamports { 200 | println!( 201 | "Note: Computed priority fee of {} exceeds the maximum priority fee", 202 | Sol(compute_budget.priority_fee_lamports()) 203 | ); 204 | ComputeBudget::new(compute_unit_limit, max_lamports) 205 | } else { 206 | compute_budget 207 | } 208 | } 209 | }; 210 | 211 | println!( 212 | "Selected priority fee: {} (for {} CUs)", 213 | Sol(compute_budget.priority_fee_lamports()), 214 | compute_budget.compute_unit_limit, 215 | ); 216 | assert!( 217 | 0.2 > lamports_to_sol(compute_budget.priority_fee_lamports()), 218 | "Priority fee too large, Bug?" 219 | ); 220 | 221 | assert_ne!(compute_budget.compute_unit_limit, 0); 222 | instructions.push( 223 | compute_budget::ComputeBudgetInstruction::set_compute_unit_limit( 224 | compute_budget.compute_unit_limit, 225 | ), 226 | ); 227 | 228 | if compute_budget.compute_unit_price_micro_lamports > 0 { 229 | instructions.push( 230 | compute_budget::ComputeBudgetInstruction::set_compute_unit_price( 231 | compute_budget.compute_unit_price_micro_lamports, 232 | ), 233 | ); 234 | } 235 | 236 | Ok(compute_budget.priority_fee_lamports()) 237 | } 238 | -------------------------------------------------------------------------------- /src/vendor/drift/spot_market.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey::Pubkey; 2 | 3 | pub const SIZE: usize = 776; 4 | 5 | #[derive(Debug, Clone)] 6 | #[repr(C, packed(1))] 7 | pub struct SpotMarket { 8 | /// The address of the spot market. It is a pda of the market index 9 | pub pubkey: Pubkey, 10 | /// The oracle used to price the markets deposits/borrows 11 | pub oracle: Pubkey, 12 | /// The token mint of the market 13 | pub mint: Pubkey, 14 | /// The vault used to store the market's deposits 15 | /// The amount in the vault should be equal to or greater than deposits - borrows 16 | pub vault: Pubkey, 17 | /// The encoded display name for the market e.g. SOL 18 | pub name: [u8; 32], 19 | pub historical_oracle_data: HistoricalOracleData, 20 | pub historical_index_data: HistoricalIndexData, 21 | /// Revenue the protocol has collected in this markets token 22 | /// e.g. for SOL-PERP, funds can be settled in usdc and will flow into the USDC revenue pool 23 | pub revenue_pool: PoolBalance, // in base asset 24 | /// The fees collected from swaps between this market and the quote market 25 | /// Is settled to the quote markets revenue pool 26 | pub spot_fee_pool: PoolBalance, 27 | /// Details on the insurance fund covering bankruptcies in this markets token 28 | /// Covers bankruptcies for borrows with this markets token and perps settling in this markets token 29 | pub insurance_fund: InsuranceFund, 30 | /// The total spot fees collected for this market 31 | /// precision: QUOTE_PRECISION 32 | pub total_spot_fee: u128, 33 | /// The sum of the scaled balances for deposits across users and pool balances 34 | /// To convert to the deposit token amount, multiply by the cumulative deposit interest 35 | /// precision: SPOT_BALANCE_PRECISION 36 | pub deposit_balance: u128, 37 | /// The sum of the scaled balances for borrows across users and pool balances 38 | /// To convert to the borrow token amount, multiply by the cumulative borrow interest 39 | /// precision: SPOT_BALANCE_PRECISION 40 | pub borrow_balance: u128, 41 | /// The cumulative interest earned by depositors 42 | /// Used to calculate the deposit token amount from the deposit balance 43 | /// precision: SPOT_CUMULATIVE_INTEREST_PRECISION 44 | pub cumulative_deposit_interest: u128, 45 | /// The cumulative interest earned by borrowers 46 | /// Used to calculate the borrow token amount from the borrow balance 47 | /// precision: SPOT_CUMULATIVE_INTEREST_PRECISION 48 | pub cumulative_borrow_interest: u128, 49 | /// The total socialized loss from borrows, in the mint's token 50 | /// precision: token mint precision 51 | pub total_social_loss: u128, 52 | /// The total socialized loss from borrows, in the quote market's token 53 | /// preicision: QUOTE_PRECISION 54 | pub total_quote_social_loss: u128, 55 | /// no withdraw limits/guards when deposits below this threshold 56 | /// precision: token mint precision 57 | pub withdraw_guard_threshold: u64, 58 | /// The max amount of token deposits in this market 59 | /// 0 if there is no limit 60 | /// precision: token mint precision 61 | pub max_token_deposits: u64, 62 | /// 24hr average of deposit token amount 63 | /// precision: token mint precision 64 | pub deposit_token_twap: u64, 65 | /// 24hr average of borrow token amount 66 | /// precision: token mint precision 67 | pub borrow_token_twap: u64, 68 | /// 24hr average of utilization 69 | /// which is borrow amount over token amount 70 | /// precision: SPOT_UTILIZATION_PRECISION 71 | pub utilization_twap: u64, 72 | /// Last time the cumulative deposit and borrow interest was updated 73 | pub last_interest_ts: u64, 74 | /// Last time the deposit/borrow/utilization averages were updated 75 | pub last_twap_ts: u64, 76 | /// The time the market is set to expire. Only set if market is in reduce only mode 77 | pub expiry_ts: i64, 78 | /// Spot orders must be a multiple of the step size 79 | /// precision: token mint precision 80 | pub order_step_size: u64, 81 | /// Spot orders must be a multiple of the tick size 82 | /// precision: PRICE_PRECISION 83 | pub order_tick_size: u64, 84 | /// The minimum order size 85 | /// precision: token mint precision 86 | pub min_order_size: u64, 87 | /// The maximum spot position size 88 | /// if the limit is 0, there is no limit 89 | /// precision: token mint precision 90 | pub max_position_size: u64, 91 | /// Every spot trade has a fill record id. This is the next id to use 92 | pub next_fill_record_id: u64, 93 | /// Every deposit has a deposit record id. This is the next id to use 94 | pub next_deposit_record_id: u64, 95 | /// The initial asset weight used to calculate a deposits contribution to a users initial total collateral 96 | /// e.g. if the asset weight is .8, $100 of deposits contributes $80 to the users initial total collateral 97 | /// precision: SPOT_WEIGHT_PRECISION 98 | pub initial_asset_weight: u32, 99 | /// The maintenance asset weight used to calculate a deposits contribution to a users maintenance total collateral 100 | /// e.g. if the asset weight is .9, $100 of deposits contributes $90 to the users maintenance total collateral 101 | /// precision: SPOT_WEIGHT_PRECISION 102 | pub maintenance_asset_weight: u32, 103 | /// The initial liability weight used to calculate a borrows contribution to a users initial margin requirement 104 | /// e.g. if the liability weight is .9, $100 of borrows contributes $90 to the users initial margin requirement 105 | /// precision: SPOT_WEIGHT_PRECISION 106 | pub initial_liability_weight: u32, 107 | /// The maintenance liability weight used to calculate a borrows contribution to a users maintenance margin requirement 108 | /// e.g. if the liability weight is .8, $100 of borrows contributes $80 to the users maintenance margin requirement 109 | /// precision: SPOT_WEIGHT_PRECISION 110 | pub maintenance_liability_weight: u32, 111 | /// The initial margin fraction factor. Used to increase liability weight/decrease asset weight for large positions 112 | /// precision: MARGIN_PRECISION 113 | pub imf_factor: u32, 114 | /// The fee the liquidator is paid for taking over borrow/deposit 115 | /// precision: LIQUIDATOR_FEE_PRECISION 116 | pub liquidator_fee: u32, 117 | /// The fee the insurance fund receives from liquidation 118 | /// precision: LIQUIDATOR_FEE_PRECISION 119 | pub if_liquidation_fee: u32, 120 | /// The optimal utilization rate for this market. 121 | /// Used to determine the markets borrow rate 122 | /// precision: SPOT_UTILIZATION_PRECISION 123 | pub optimal_utilization: u32, 124 | /// The borrow rate for this market when the market has optimal utilization 125 | /// precision: SPOT_RATE_PRECISION 126 | pub optimal_borrow_rate: u32, 127 | /// The borrow rate for this market when the market has 1000 utilization 128 | /// precision: SPOT_RATE_PRECISION 129 | pub max_borrow_rate: u32, 130 | /// The market's token mint's decimals. To from decimals to a precision, 10^decimals 131 | pub decimals: u32, 132 | pub market_index: u16, 133 | /// Whether or not spot trading is enabled 134 | pub orders_enabled: bool, 135 | pub oracle_source: OracleSource, 136 | pub status: MarketStatus, 137 | /// The asset tier affects how a deposit can be used as collateral and the priority for a borrow being liquidated 138 | pub asset_tier: AssetTier, 139 | pub paused_operations: u8, 140 | pub if_paused_operations: u8, 141 | pub fee_adjustment: i16, 142 | pub padding1: [u8; 2], 143 | /// For swaps, the amount of token loaned out in the begin_swap ix 144 | /// precision: token mint precision 145 | pub flash_loan_amount: u64, 146 | /// For swaps, the amount in the users token account in the begin_swap ix 147 | /// Used to calculate how much of the token left the system in end_swap ix 148 | /// precision: token mint precision 149 | pub flash_loan_initial_token_amount: u64, 150 | /// The total fees received from swaps 151 | /// precision: token mint precision 152 | pub total_swap_fee: u64, 153 | /// When to begin scaling down the initial asset weight 154 | /// disabled when 0 155 | /// precision: QUOTE_PRECISION 156 | pub scale_initial_asset_weight_start: u64, 157 | pub padding: [u8; 48], 158 | } 159 | 160 | #[derive(Clone, Copy, PartialEq, Debug, Eq, PartialOrd, Ord)] 161 | pub enum AssetTier { 162 | /// full priviledge 163 | Collateral, 164 | /// collateral, but no borrow 165 | Protected, 166 | /// not collateral, allow multi-borrow 167 | Cross, 168 | /// not collateral, only single borrow 169 | Isolated, 170 | /// no privilege 171 | Unlisted, 172 | } 173 | 174 | #[derive(Clone, Copy, PartialEq, Debug, Eq)] 175 | pub enum MarketStatus { 176 | /// warm up period for initialization, fills are paused 177 | Initialized, 178 | /// all operations allowed 179 | Active, 180 | /// Deprecated in favor of PausedOperations 181 | FundingPaused, 182 | /// Deprecated in favor of PausedOperations 183 | AmmPaused, 184 | /// Deprecated in favor of PausedOperations 185 | FillPaused, 186 | /// Deprecated in favor of PausedOperations 187 | WithdrawPaused, 188 | /// fills only able to reduce liability 189 | ReduceOnly, 190 | /// market has determined settlement price and positions are expired must be settled 191 | Settlement, 192 | /// market has no remaining participants 193 | Delisted, 194 | } 195 | 196 | #[derive(Debug, Clone, Copy)] 197 | #[repr(C, packed(1))] 198 | pub struct PoolBalance { 199 | /// To get the pool's token amount, you must multiply the scaled balance by the market's cumulative 200 | /// deposit interest 201 | /// precision: SPOT_BALANCE_PRECISION 202 | pub scaled_balance: u128, 203 | /// The spot market the pool is for 204 | pub market_index: u16, 205 | pub padding: [u8; 6], 206 | } 207 | 208 | #[derive(Clone, Copy, Eq, PartialEq, Debug)] 209 | pub enum OracleSource { 210 | Pyth, 211 | Switchboard, 212 | QuoteAsset, 213 | Pyth1K, 214 | Pyth1M, 215 | PythStableCoin, 216 | Prelaunch, 217 | PythPull, 218 | Pyth1KPull, 219 | Pyth1MPull, 220 | PythStableCoinPull, 221 | } 222 | 223 | #[derive(Clone, Copy, Eq, PartialEq, Debug)] 224 | #[repr(C, packed(1))] 225 | pub struct HistoricalOracleData { 226 | /// precision: PRICE_PRECISION 227 | pub last_oracle_price: i64, 228 | /// precision: PRICE_PRECISION 229 | pub last_oracle_conf: u64, 230 | /// number of slots since last update 231 | pub last_oracle_delay: i64, 232 | /// precision: PRICE_PRECISION 233 | pub last_oracle_price_twap: i64, 234 | /// precision: PRICE_PRECISION 235 | pub last_oracle_price_twap_5min: i64, 236 | /// unix_timestamp of last snapshot 237 | pub last_oracle_price_twap_ts: i64, 238 | } 239 | 240 | #[derive(Debug, Clone, Copy)] 241 | #[repr(C, packed(1))] 242 | pub struct HistoricalIndexData { 243 | /// precision: PRICE_PRECISION 244 | pub last_index_bid_price: u64, 245 | /// precision: PRICE_PRECISION 246 | pub last_index_ask_price: u64, 247 | /// precision: PRICE_PRECISION 248 | pub last_index_price_twap: u64, 249 | /// precision: PRICE_PRECISION 250 | pub last_index_price_twap_5min: u64, 251 | /// unix_timestamp of last snapshot 252 | pub last_index_price_twap_ts: i64, 253 | } 254 | 255 | #[derive(Debug, Clone, Copy)] 256 | #[repr(C, packed(1))] 257 | pub struct InsuranceFund { 258 | pub vault: Pubkey, 259 | pub total_shares: u128, 260 | pub user_shares: u128, 261 | pub shares_base: u128, // exponent for lp shares (for rebasing) 262 | pub unstaking_period: i64, // if_unstaking_period 263 | pub last_revenue_settle_ts: i64, 264 | pub revenue_settle_period: i64, 265 | pub total_factor: u32, // percentage of interest for total insurance 266 | pub user_factor: u32, // percentage of interest for user staked insurance 267 | } 268 | -------------------------------------------------------------------------------- /src/vendor/marginfi_v2.rs: -------------------------------------------------------------------------------- 1 | /// MarginFi v2 bits yanked from https://github.com/mrgnlabs/marginfi-v2/tree/main/programs/marginfi/src 2 | use { 3 | fixed::types::I80F48, 4 | solana_sdk::pubkey::Pubkey, 5 | std::fmt::{Debug, Formatter}, 6 | }; 7 | 8 | const MAX_ORACLE_KEYS: usize = 5; 9 | 10 | /// Value where total_asset_value_init_limit is considered inactive 11 | const TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE: u64 = 0; 12 | 13 | macro_rules! assert_struct_size { 14 | ($struct: ty, $size: expr) => { 15 | static_assertions::const_assert_eq!(std::mem::size_of::<$struct>(), $size); 16 | }; 17 | } 18 | 19 | macro_rules! assert_struct_align { 20 | ($struct: ty, $align: expr) => { 21 | static_assertions::const_assert_eq!(std::mem::align_of::<$struct>(), $align); 22 | }; 23 | } 24 | 25 | assert_struct_size!(Bank, 1856); 26 | assert_struct_align!(Bank, 8); 27 | #[repr(C)] 28 | #[derive(Default, Debug)] 29 | pub struct Bank { 30 | pub mint: Pubkey, 31 | pub mint_decimals: u8, 32 | 33 | pub group: Pubkey, 34 | 35 | pub asset_share_value: WrappedI80F48, 36 | pub liability_share_value: WrappedI80F48, 37 | 38 | pub liquidity_vault: Pubkey, 39 | pub liquidity_vault_bump: u8, 40 | pub liquidity_vault_authority_bump: u8, 41 | 42 | pub insurance_vault: Pubkey, 43 | pub insurance_vault_bump: u8, 44 | pub insurance_vault_authority_bump: u8, 45 | pub collected_insurance_fees_outstanding: WrappedI80F48, 46 | 47 | pub fee_vault: Pubkey, 48 | pub fee_vault_bump: u8, 49 | pub fee_vault_authority_bump: u8, 50 | pub collected_group_fees_outstanding: WrappedI80F48, 51 | 52 | pub total_liability_shares: WrappedI80F48, 53 | pub total_asset_shares: WrappedI80F48, 54 | 55 | pub last_update: i64, 56 | 57 | pub config: BankConfig, 58 | 59 | /// Emissions Config Flags 60 | /// 61 | /// - EMISSIONS_FLAG_BORROW_ACTIVE: 1 62 | /// - EMISSIONS_FLAG_LENDING_ACTIVE: 2 63 | /// 64 | pub emissions_flags: u64, 65 | /// Emissions APR. 66 | /// Number of emitted tokens (emissions_mint) per 1e(bank.mint_decimal) tokens (bank mint) (native amount) per 1 YEAR. 67 | pub emissions_rate: u64, 68 | pub emissions_remaining: WrappedI80F48, 69 | pub emissions_mint: Pubkey, 70 | 71 | pub _padding_0: [[u64; 2]; 28], 72 | pub _padding_1: [[u64; 2]; 32], // 16 * 2 * 32 = 1024B 73 | } 74 | 75 | impl Bank { 76 | pub fn get_asset_amount(&self, shares: I80F48) -> I80F48 { 77 | shares 78 | .checked_mul(self.asset_share_value.into()) 79 | .expect("bad math") 80 | } 81 | pub fn get_liability_amount(&self, shares: I80F48) -> I80F48 { 82 | shares 83 | .checked_mul(self.liability_share_value.into()) 84 | .expect("bad math") 85 | } 86 | } 87 | 88 | #[repr(C, align(8))] 89 | #[derive(Default, Clone, Copy)] 90 | pub struct WrappedI80F48 { 91 | pub value: [u8; 16], 92 | } 93 | 94 | impl Debug for WrappedI80F48 { 95 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 96 | write!(f, "{}", I80F48::from_le_bytes(self.value)) 97 | } 98 | } 99 | 100 | impl From for WrappedI80F48 { 101 | fn from(i: I80F48) -> Self { 102 | Self { 103 | value: i.to_le_bytes(), 104 | } 105 | } 106 | } 107 | 108 | impl From for I80F48 { 109 | fn from(w: WrappedI80F48) -> Self { 110 | Self::from_le_bytes(w.value) 111 | } 112 | } 113 | 114 | #[repr(u8)] 115 | #[derive(Copy, Clone, Debug)] 116 | pub enum BankOperationalState { 117 | Paused, 118 | Operational, 119 | ReduceOnly, 120 | } 121 | 122 | #[repr(C)] 123 | #[derive(Default, Debug)] 124 | pub struct InterestRateConfig { 125 | // Curve Params 126 | pub optimal_utilization_rate: WrappedI80F48, 127 | pub plateau_interest_rate: WrappedI80F48, 128 | pub max_interest_rate: WrappedI80F48, 129 | 130 | // Fees 131 | pub insurance_fee_fixed_apr: WrappedI80F48, 132 | pub insurance_ir_fee: WrappedI80F48, 133 | pub protocol_fixed_fee_apr: WrappedI80F48, 134 | pub protocol_ir_fee: WrappedI80F48, 135 | 136 | pub _padding: [[u64; 2]; 8], // 16 * 8 = 128 bytes 137 | } 138 | 139 | /// Calculates the fee rate for a given base rate and fees specified. 140 | /// The returned rate is only the fee rate without the base rate. 141 | /// 142 | /// Used for calculating the fees charged to the borrowers. 143 | fn calc_fee_rate(base_rate: I80F48, rate_fees: I80F48, fixed_fees: I80F48) -> Option { 144 | base_rate.checked_mul(rate_fees)?.checked_add(fixed_fees) 145 | } 146 | 147 | impl InterestRateConfig { 148 | /// Return interest rate charged to borrowers and to depositors. 149 | /// Rate is denominated in APR (0-). 150 | /// 151 | /// Return (`lending_rate`, `borrowing_rate`, `group_fees_apr`, `insurance_fees_apr`) 152 | pub fn calc_interest_rate( 153 | &self, 154 | utilization_ratio: I80F48, 155 | ) -> Option<(I80F48, I80F48, I80F48, I80F48)> { 156 | let protocol_ir_fee = I80F48::from(self.protocol_ir_fee); 157 | let insurance_ir_fee = I80F48::from(self.insurance_ir_fee); 158 | 159 | let protocol_fixed_fee_apr = I80F48::from(self.protocol_fixed_fee_apr); 160 | let insurance_fee_fixed_apr = I80F48::from(self.insurance_fee_fixed_apr); 161 | 162 | let rate_fee = protocol_ir_fee + insurance_ir_fee; 163 | let total_fixed_fee_apr = protocol_fixed_fee_apr + insurance_fee_fixed_apr; 164 | 165 | let base_rate = self.interest_rate_curve(utilization_ratio)?; 166 | 167 | // Lending rate is adjusted for utilization ratio to symmetrize payments between borrowers and depositors. 168 | let lending_rate = base_rate.checked_mul(utilization_ratio)?; 169 | 170 | // Borrowing rate is adjusted for fees. 171 | // borrowing_rate = base_rate + base_rate * rate_fee + total_fixed_fee_apr 172 | let borrowing_rate = base_rate 173 | .checked_mul(I80F48::ONE.checked_add(rate_fee)?)? 174 | .checked_add(total_fixed_fee_apr)?; 175 | 176 | let group_fees_apr = calc_fee_rate( 177 | base_rate, 178 | self.protocol_ir_fee.into(), 179 | self.protocol_fixed_fee_apr.into(), 180 | )?; 181 | 182 | let insurance_fees_apr = calc_fee_rate( 183 | base_rate, 184 | self.insurance_ir_fee.into(), 185 | self.insurance_fee_fixed_apr.into(), 186 | )?; 187 | 188 | assert!(lending_rate >= I80F48::ZERO); 189 | assert!(borrowing_rate >= I80F48::ZERO); 190 | assert!(group_fees_apr >= I80F48::ZERO); 191 | assert!(insurance_fees_apr >= I80F48::ZERO); 192 | 193 | // TODO: Add liquidation discount check 194 | 195 | Some(( 196 | lending_rate, 197 | borrowing_rate, 198 | group_fees_apr, 199 | insurance_fees_apr, 200 | )) 201 | } 202 | 203 | /// Piecewise linear interest rate function. 204 | /// The curves approaches the `plateau_interest_rate` as the utilization ratio approaches the `optimal_utilization_rate`, 205 | /// once the utilization ratio exceeds the `optimal_utilization_rate`, the curve approaches the `max_interest_rate`. 206 | /// 207 | /// To be clear we don't particularly appreciate the piecewise linear nature of this "curve", but it is what it is. 208 | fn interest_rate_curve(&self, ur: I80F48) -> Option { 209 | let optimal_ur = self.optimal_utilization_rate.into(); 210 | let plateau_ir = self.plateau_interest_rate.into(); 211 | let max_ir: I80F48 = self.max_interest_rate.into(); 212 | 213 | if ur <= optimal_ur { 214 | ur.checked_div(optimal_ur)?.checked_mul(plateau_ir) 215 | } else { 216 | (ur - optimal_ur) 217 | .checked_div(I80F48::ONE - optimal_ur)? 218 | .checked_mul(max_ir - plateau_ir)? 219 | .checked_add(plateau_ir) 220 | } 221 | } 222 | } 223 | 224 | #[repr(u8)] 225 | #[derive(Copy, Clone, Debug)] 226 | pub enum OracleSetup { 227 | None, 228 | PythEma, 229 | SwitchboardV2, 230 | } 231 | 232 | #[repr(u64)] 233 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 234 | pub enum RiskTier { 235 | Collateral, 236 | /// ## Isolated Risk 237 | /// Assets in this trance can be borrowed only in isolation. 238 | /// They can't be borrowed together with other assets. 239 | /// 240 | /// For example, if users has USDC, and wants to borrow XYZ which is isolated, 241 | /// they can't borrow XYZ together with SOL, only XYZ alone. 242 | Isolated, 243 | } 244 | 245 | assert_struct_size!(BankConfig, 544); 246 | assert_struct_align!(BankConfig, 8); 247 | #[repr(C)] 248 | #[derive(Debug)] 249 | /// TODO: Convert weights to (u64, u64) to avoid precision loss (maybe?) 250 | pub struct BankConfig { 251 | pub asset_weight_init: WrappedI80F48, 252 | pub asset_weight_maint: WrappedI80F48, 253 | 254 | pub liability_weight_init: WrappedI80F48, 255 | pub liability_weight_maint: WrappedI80F48, 256 | 257 | pub deposit_limit: u64, 258 | 259 | pub interest_rate_config: InterestRateConfig, 260 | pub operational_state: BankOperationalState, 261 | 262 | pub oracle_setup: OracleSetup, 263 | pub oracle_keys: [Pubkey; MAX_ORACLE_KEYS], 264 | 265 | pub borrow_limit: u64, 266 | 267 | pub risk_tier: RiskTier, 268 | 269 | /// USD denominated limit for calculating asset value for initialization margin requirements. 270 | /// Example, if total SOL deposits are equal to $1M and the limit it set to $500K, 271 | /// then SOL assets will be discounted by 50%. 272 | /// 273 | /// In other words the max value of liabilities that can be backed by the asset is $500K. 274 | /// This is useful for limiting the damage of orcale attacks. 275 | /// 276 | /// Value is UI USD value, for example value 100 -> $100 277 | pub total_asset_value_init_limit: u64, 278 | 279 | /// Time window in seconds for the oracle price feed to be considered live. 280 | pub oracle_max_age: u16, 281 | 282 | pub _padding: [u16; 19], // 16 * 4 = 64 bytes 283 | } 284 | 285 | impl Default for BankConfig { 286 | fn default() -> Self { 287 | Self { 288 | asset_weight_init: I80F48::ZERO.into(), 289 | asset_weight_maint: I80F48::ZERO.into(), 290 | liability_weight_init: I80F48::ONE.into(), 291 | liability_weight_maint: I80F48::ONE.into(), 292 | deposit_limit: 0, 293 | borrow_limit: 0, 294 | interest_rate_config: Default::default(), 295 | operational_state: BankOperationalState::Paused, 296 | oracle_setup: OracleSetup::None, 297 | oracle_keys: [Pubkey::default(); MAX_ORACLE_KEYS], 298 | risk_tier: RiskTier::Isolated, 299 | total_asset_value_init_limit: TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE, 300 | oracle_max_age: 0, 301 | _padding: [0; 19], 302 | } 303 | } 304 | } 305 | 306 | assert_struct_size!(MarginfiAccount, 2304); 307 | assert_struct_align!(MarginfiAccount, 8); 308 | #[repr(C)] 309 | #[derive(Debug)] 310 | pub struct MarginfiAccount { 311 | pub group: Pubkey, // 32 312 | pub authority: Pubkey, // 32 313 | pub lending_account: LendingAccount, // 1728 314 | /// The flags that indicate the state of the account. 315 | /// This is u64 bitfield, where each bit represents a flag. 316 | /// 317 | /// Flags: 318 | /// - DISABLED_FLAG = 1 << 0 = 1 - This flag indicates that the account is disabled, and no further actions can be taken on it. 319 | pub account_flags: u64, // 8 320 | pub _padding: [u64; 63], // 8 * 63 = 512 321 | } 322 | 323 | const MAX_LENDING_ACCOUNT_BALANCES: usize = 16; 324 | 325 | assert_struct_size!(LendingAccount, 1728); 326 | assert_struct_align!(LendingAccount, 8); 327 | #[repr(C)] 328 | #[derive(Debug)] 329 | pub struct LendingAccount { 330 | pub balances: [Balance; MAX_LENDING_ACCOUNT_BALANCES], // 104 * 16 = 1664 331 | pub _padding: [u64; 8], // 8 * 8 = 64 332 | } 333 | 334 | impl LendingAccount { 335 | pub fn get_first_empty_balance(&self) -> Option { 336 | self.balances.iter().position(|b| !b.active) 337 | } 338 | } 339 | 340 | impl LendingAccount { 341 | pub fn get_balance(&self, bank_pk: &Pubkey) -> Option<&Balance> { 342 | self.balances 343 | .iter() 344 | .find(|balance| balance.active && balance.bank_pk.eq(bank_pk)) 345 | } 346 | } 347 | 348 | assert_struct_size!(Balance, 104); 349 | assert_struct_align!(Balance, 8); 350 | #[repr(C)] 351 | #[derive(Debug)] 352 | pub struct Balance { 353 | pub active: bool, 354 | pub bank_pk: Pubkey, 355 | pub asset_shares: WrappedI80F48, 356 | pub liability_shares: WrappedI80F48, 357 | pub emissions_outstanding: WrappedI80F48, 358 | pub last_update: u64, 359 | pub _padding: [u64; 1], 360 | } 361 | -------------------------------------------------------------------------------- /src/kraken_exchange.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{exchange::*, token::MaybeToken}, 3 | async_trait::async_trait, 4 | chrono::prelude::*, 5 | kraken_sdk_rest::Client, 6 | rust_decimal::prelude::*, 7 | solana_sdk::pubkey::Pubkey, 8 | std::collections::HashMap, 9 | }; 10 | 11 | pub struct KrakenExchangeClient { 12 | client: Client, 13 | } 14 | 15 | fn normalize_coin_name(kraken_coin: &str) -> &str { 16 | if kraken_coin == "ZUSD" { 17 | "USD" 18 | } else { 19 | kraken_coin 20 | } 21 | } 22 | 23 | fn deposit_methods() -> HashMap { 24 | HashMap::from([ 25 | ("SOL", "Solana"), 26 | ("USDC", "USDC (SPL)"), 27 | ("mSOL", "Marinade SOL (mSOL)"), 28 | ]) 29 | } 30 | 31 | #[async_trait] 32 | impl ExchangeClient for KrakenExchangeClient { 33 | async fn deposit_address( 34 | &self, 35 | token: MaybeToken, 36 | ) -> Result> { 37 | let deposit_method = *deposit_methods().get(token.name()).ok_or_else(|| { 38 | //dbg!(self.client.get_deposit_methods(token.to_string()).send().await?); 39 | format!("Unsupported deposit token: {}", token.name()) 40 | })?; 41 | 42 | let deposit_addresses = self 43 | .client 44 | .get_deposit_addresses(token.to_string(), deposit_method) 45 | .send() 46 | .await?; 47 | 48 | assert_eq!(deposit_addresses.len(), 1); // TODO: Consider what to do with multiple deposit addresses 49 | 50 | Ok(deposit_addresses[0].address.parse::()?) 51 | } 52 | 53 | async fn balances( 54 | &self, 55 | ) -> Result, Box> { 56 | //dbg!(self.client.get_open_orders().send().await?); 57 | let open_orders = self.client.get_open_orders().send().await?; 58 | 59 | // TODO: Generalize the `in_order_sol`/`in_order_usd` handling to all coins held by the 60 | // account 61 | let mut in_order_sol = 0.; 62 | let mut in_order_usd = 0.; 63 | 64 | for open_order in open_orders.open.values() { 65 | assert_eq!(open_order.status, "open"); // TODO: What other statuses are valid, if any? 66 | if open_order.descr.pair == self.preferred_solusd_pair() { 67 | let vol = open_order 68 | .vol 69 | .parse::() 70 | .map_err(|err| format!("Invalid open order `vol` field: {err}"))?; 71 | let price = open_order 72 | .descr 73 | .price 74 | .parse::() 75 | .map_err(|err| format!("Invalid open order `descr.price` field: {err}"))?; 76 | if open_order.descr.orderside == "sell" { 77 | in_order_sol += vol 78 | } else { 79 | in_order_usd += vol * price; 80 | } 81 | } 82 | } 83 | 84 | Ok(self 85 | .client 86 | .get_account_balance() 87 | .send() 88 | .await? 89 | .into_iter() 90 | .filter_map(|(coin, balance)| { 91 | balance 92 | .parse::() 93 | .ok() 94 | .and_then(|balance| match coin.as_str() { 95 | "SOL" => { 96 | assert!(balance >= in_order_sol); 97 | Some(ExchangeBalance { 98 | total: balance, 99 | available: balance - in_order_sol, 100 | }) 101 | } 102 | "USDC" => Some(ExchangeBalance { 103 | total: balance, 104 | available: balance, 105 | }), 106 | "ZUSD" => { 107 | assert!(balance >= in_order_usd); 108 | Some(ExchangeBalance { 109 | total: balance, 110 | available: balance - in_order_usd, 111 | }) 112 | } 113 | _ => None, 114 | }) 115 | .map(|exchange_balance| (normalize_coin_name(&coin).into(), exchange_balance)) 116 | }) 117 | .collect()) 118 | } 119 | 120 | async fn recent_deposits( 121 | &self, 122 | ) -> Result>, Box> { 123 | let mut successful_deposits = vec![]; 124 | 125 | for coin in deposit_methods().keys() { 126 | for deposit_status in self.client.get_deposit_status(*coin).send().await? { 127 | //dbg!(&deposit_status); 128 | if deposit_status.status == "Success" { 129 | successful_deposits.push(DepositInfo { 130 | tx_id: deposit_status.txid, 131 | amount: deposit_status.amount.parse::().unwrap(), 132 | }); 133 | } 134 | } 135 | } 136 | Ok(Some(successful_deposits)) 137 | } 138 | 139 | async fn recent_withdrawals(&self) -> Result, Box> { 140 | // Withdrawals not currently supported for Kraken 141 | Ok(vec![]) 142 | } 143 | 144 | async fn request_withdraw( 145 | &self, 146 | _address: Pubkey, 147 | _token: MaybeToken, 148 | _amount: f64, 149 | _password: Option, 150 | _code: Option, 151 | ) -> Result<(/* withdraw_id: */ String, /*withdraw_fee: */ f64), Box> 152 | { 153 | Err("Withdrawals not currently supported for Kraken".into()) 154 | } 155 | 156 | async fn print_market_info( 157 | &self, 158 | pair: &str, 159 | format: MarketInfoFormat, 160 | ) -> Result<(), Box> { 161 | #[derive(Debug)] 162 | struct Hlv { 163 | time: DateTime, 164 | high: f64, 165 | low: f64, 166 | volume: f64, 167 | } 168 | 169 | let hourly_prices = self 170 | .client 171 | .get_ohlc_data(pair) 172 | .interval(kraken_sdk_rest::Interval::Hour1) 173 | .send() 174 | .await? 175 | .into_iter() 176 | .rev() 177 | .take(24) 178 | .filter_map(|ohlc| { 179 | if let (Some(high), Some(low), Some(volume)) = ( 180 | ohlc.high().parse::().ok(), 181 | ohlc.low().parse::().ok(), 182 | ohlc.volume().parse::().ok(), 183 | ) { 184 | #[allow(deprecated)] 185 | let naive = NaiveDateTime::from_timestamp(ohlc.time(), 0); 186 | let time: DateTime = DateTime::from_naive_utc_and_offset(naive, Utc); 187 | 188 | Some(Hlv { 189 | time, 190 | high, 191 | low, 192 | volume, 193 | }) 194 | } else { 195 | None 196 | } 197 | }) 198 | .collect::>(); 199 | 200 | if hourly_prices.len() != 24 { 201 | return Err(format!( 202 | "Failed to fetch price data for last 24 hours (fetched {} hours)", 203 | hourly_prices.len() 204 | ) 205 | .into()); 206 | } 207 | 208 | let weighted_24h_avg_price = { 209 | let mut total_volume = 0.; 210 | let mut avg_price_weighted_sum = 0.; 211 | for hourly_price in &hourly_prices { 212 | let avg_price = (hourly_price.low + hourly_price.high).to_f64().unwrap() / 2.; 213 | let volume = hourly_price.volume.to_f64().unwrap(); 214 | 215 | total_volume += volume; 216 | avg_price_weighted_sum += avg_price * volume; 217 | } 218 | 219 | avg_price_weighted_sum / total_volume 220 | }; 221 | 222 | let bid_ask = self.bid_ask(pair).await?; 223 | 224 | match format { 225 | MarketInfoFormat::All => { 226 | println!( 227 | "{} | Ask: ${:.2}, Bid: ${:.2}, 24hr Average: ${:.2}", 228 | pair, bid_ask.ask_price, bid_ask.bid_price, weighted_24h_avg_price 229 | ); 230 | } 231 | MarketInfoFormat::Ask => { 232 | println!("{}", bid_ask.ask_price); 233 | } 234 | MarketInfoFormat::Hourly => { 235 | println!("hour,low,high,average,volume"); 236 | for p in &hourly_prices { 237 | println!( 238 | "{},{},{},{},{}", 239 | DateTime::::from(p.time), 240 | p.low, 241 | p.high, 242 | (p.low + p.high).to_f64().unwrap() / 2., 243 | p.volume 244 | ); 245 | } 246 | } 247 | MarketInfoFormat::Weighted24hAveragePrice => { 248 | println!("{weighted_24h_avg_price:.4}"); 249 | } 250 | } 251 | 252 | Ok(()) 253 | } 254 | 255 | async fn bid_ask(&self, pair: &str) -> Result> { 256 | let response = self.client.get_order_book(pair).count(1).send().await?; 257 | 258 | if let Some(order_book) = response.get(pair) { 259 | if let (Some(ask_price), Some(bid_price)) = ( 260 | order_book 261 | .asks 262 | .first() 263 | .and_then(|order_book_tier| order_book_tier.0.parse::().ok()), 264 | order_book 265 | .bids 266 | .first() 267 | .and_then(|order_book_tier| order_book_tier.0.parse::().ok()), 268 | ) { 269 | return Ok(BidAsk { 270 | bid_price, 271 | ask_price, 272 | }); 273 | } 274 | } 275 | Err("Invalid API response".into()) 276 | } 277 | 278 | async fn place_order( 279 | &self, 280 | pair: &str, 281 | side: OrderSide, 282 | price: f64, 283 | amount: f64, 284 | ) -> Result> { 285 | if pair != self.preferred_solusd_pair() { 286 | // Currently only the `preferred_solusd_pair` is supported due to limitations in how 287 | // the `available` token balances are computed in `Self::balances()` 288 | return Err(format!("Unsupported trading pair: {pair}").into()); 289 | } 290 | 291 | let side = match side { 292 | OrderSide::Buy => kraken_sdk_rest::OrderSide::Buy, 293 | OrderSide::Sell => kraken_sdk_rest::OrderSide::Sell, 294 | }; 295 | 296 | let response = self 297 | .client 298 | .add_limit_order(pair, side, &amount.to_string(), &price.to_string()) 299 | .post_only() 300 | .send() 301 | .await?; 302 | //dbg!(&response); 303 | 304 | let txid = response.txid.unwrap_or_default(); 305 | assert_eq!(txid.len(), 1); 306 | Ok(txid[0].to_owned()) 307 | } 308 | 309 | async fn cancel_order( 310 | &self, 311 | _pair: &str, 312 | order_id: &OrderId, 313 | ) -> Result<(), Box> { 314 | let _ = self.client.cancel_order(order_id).send().await?; 315 | Ok(()) 316 | } 317 | 318 | async fn order_status( 319 | &self, 320 | pair: &str, 321 | order_id: &OrderId, 322 | ) -> Result> { 323 | let orders = self.client.query_orders_info(order_id).send().await?; 324 | 325 | let order = orders 326 | .get(order_id) 327 | .ok_or_else(|| format!("Unknown order id: {order_id}"))?; 328 | //dbg!(&order); 329 | 330 | assert_eq!(order.descr.ordertype, "limit"); 331 | 332 | // Currently only the `preferred_solusd_pair` is supported due to limitations in how 333 | // the `available` token balances are computed in `Self::balances()` 334 | assert_eq!(order.descr.pair, self.preferred_solusd_pair()); 335 | assert_eq!(order.descr.pair, pair); 336 | 337 | let fee = { 338 | let fee = order.fee.parse::().unwrap(); 339 | if fee > f64::EPSILON { 340 | Some((fee, "USD".to_string())) 341 | } else { 342 | None 343 | } 344 | }; 345 | 346 | // TODO: use `order.opentm` instead? 347 | let last_update = { 348 | let today = Local::now().date_naive(); 349 | NaiveDate::from_ymd_opt(today.year(), today.month(), today.day()).unwrap() 350 | }; 351 | 352 | Ok(OrderStatus { 353 | open: ["open"].contains(&order.status.as_str()), 354 | side: match order.descr.orderside.as_str() { 355 | "sell" => OrderSide::Sell, 356 | "buy" => OrderSide::Buy, 357 | side => panic!("Invalid order side: {side}"), 358 | }, 359 | price: order.descr.price.parse::().unwrap(), 360 | amount: order.vol.parse::().unwrap(), 361 | filled_amount: order.vol_exec.parse::().unwrap(), 362 | last_update, 363 | fee, 364 | }) 365 | } 366 | 367 | async fn get_lending_info( 368 | &self, 369 | _coin: &str, 370 | ) -> Result, Box> { 371 | Err("Lending not currently supported for Kraken".into()) 372 | } 373 | 374 | async fn get_lending_history( 375 | &self, 376 | _lending_history: LendingHistory, 377 | ) -> Result, Box> { 378 | Err("Lending not currently supported for Kraken".into()) 379 | } 380 | 381 | async fn submit_lending_offer( 382 | &self, 383 | _coin: &str, 384 | _size: f64, 385 | ) -> Result<(), Box> { 386 | Err("Lending not currently supported for Kraken".into()) 387 | } 388 | 389 | fn preferred_solusd_pair(&self) -> &'static str { 390 | "SOLUSD" 391 | } 392 | } 393 | 394 | pub fn new( 395 | ExchangeCredentials { 396 | api_key, 397 | secret, 398 | subaccount, 399 | }: ExchangeCredentials, 400 | ) -> Result> { 401 | if subaccount.is_some() { 402 | return Err("subaccounts not supported".into()); 403 | } 404 | 405 | Ok(KrakenExchangeClient { 406 | client: Client::new(&api_key, &secret), 407 | }) 408 | } 409 | -------------------------------------------------------------------------------- /src/binance_exchange.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{exchange::*, token::MaybeToken, token::Token}, 3 | async_trait::async_trait, 4 | chrono::{Local, TimeZone}, 5 | solana_sdk::pubkey::Pubkey, 6 | std::{ 7 | collections::HashMap, 8 | str::FromStr, 9 | time::{SystemTime, UNIX_EPOCH}, 10 | }, 11 | }; 12 | 13 | pub struct BinanceExchangeClient { 14 | account: binance::account::Account, 15 | market: binance::market::Market, 16 | wallet: binance::wallet::Wallet, 17 | preferred_solusd_pair: &'static str, 18 | } 19 | 20 | #[async_trait] 21 | impl ExchangeClient for BinanceExchangeClient { 22 | async fn deposit_address( 23 | &self, 24 | token: MaybeToken, 25 | ) -> Result> { 26 | if token != MaybeToken::SOL() { 27 | return Err(format!("{token} deposits are not supported").into()); 28 | } 29 | 30 | if !self.account.get_account().await?.can_deposit { 31 | return Err("deposits not available".into()); 32 | } 33 | 34 | Ok(self 35 | .wallet 36 | .deposit_address(binance::rest_model::DepositAddressQuery { 37 | coin: "SOL".into(), 38 | network: None, 39 | }) 40 | .await? 41 | .address 42 | .parse::()?) 43 | } 44 | 45 | async fn recent_deposits( 46 | &self, 47 | ) -> Result>, Box> { 48 | Ok(Some( 49 | self.wallet 50 | .deposit_history(&binance::rest_model::DepositHistoryQuery::default()) 51 | .await? 52 | .into_iter() 53 | .filter_map(|dr| { 54 | /* status codes: 0 = pending, 6 = credited but cannot withdraw, 1 = success */ 55 | if dr.status == 1 { 56 | Some(DepositInfo { 57 | tx_id: dr.tx_id, 58 | amount: dr.amount, 59 | }) 60 | } else { 61 | None 62 | } 63 | }) 64 | .collect(), 65 | )) 66 | } 67 | 68 | async fn recent_withdrawals(&self) -> Result, Box> { 69 | Ok(self 70 | .wallet 71 | .withdraw_history(&binance::rest_model::WithdrawalHistoryQuery::default()) 72 | .await? 73 | .into_iter() 74 | .map(|wr| { 75 | /* status codes: 0 = email sent, 1 = canceled, 2 = awaiting approval, 76 | 3 = rejected, 4 = processing, 5 = failure, 77 | 6 = completed */ 78 | let (completed, tx_id) = match wr.status { 79 | 6 => (true, Some(wr.tx_id.expect("transaction id"))), 80 | 1 => (true, None), 81 | _ => (false, None), 82 | }; 83 | 84 | let token = if &wr.coin == "SOL" { 85 | None 86 | } else { 87 | Token::from_str(&wr.coin).ok() 88 | }; 89 | WithdrawalInfo { 90 | address: wr.address.parse::().unwrap_or_default(), 91 | token: token.into(), 92 | amount: wr.amount, 93 | tag: wr.withdraw_order_id.unwrap_or_default(), 94 | completed, 95 | tx_id, 96 | } 97 | }) 98 | .collect()) 99 | } 100 | 101 | async fn request_withdraw( 102 | &self, 103 | address: Pubkey, 104 | token: MaybeToken, 105 | amount: f64, 106 | _withdrawal_password: Option, 107 | _withdrawal_code: Option, 108 | ) -> Result<(/* withdraw_id: */ String, /*withdraw_fee: */ f64), Box> 109 | { 110 | if token != MaybeToken::SOL() { 111 | return Err(format!("{token} deposits are not supported").into()); 112 | } 113 | 114 | let sol_info = self 115 | .wallet 116 | .all_coin_info() 117 | .await? 118 | .into_iter() 119 | .find(|ci| ci.coin == "SOL") 120 | .ok_or("SOL not found in Binance coin list")?; 121 | 122 | if !sol_info.deposit_all_enable { 123 | return Err("SOL deposits not enabled".into()); 124 | } 125 | 126 | let sol_network_info = &sol_info.network_list[0]; 127 | assert_eq!(&sol_network_info.network, "SOL"); 128 | assert_eq!(&sol_network_info.name, "Solana"); 129 | assert_eq!(&sol_network_info.coin, "SOL"); 130 | 131 | if !sol_network_info.deposit_enable { 132 | return Err(format!( 133 | "Binance deposits disabled: {}", 134 | sol_network_info.deposit_desc 135 | ) 136 | .into()); 137 | } 138 | 139 | if !sol_network_info.withdraw_enable { 140 | return Err(format!( 141 | "Binance withdrawals disabled: {}", 142 | sol_network_info.withdraw_desc 143 | ) 144 | .into()); 145 | } 146 | 147 | if amount < sol_network_info.withdraw_min { 148 | return Err(format!( 149 | "Withdrawal request is below the minimum of {} SOL", 150 | sol_network_info.withdraw_min 151 | ) 152 | .into()); 153 | } 154 | 155 | let withdraw_fee = sol_network_info.withdraw_fee; 156 | 157 | let withdraw_order_id = SystemTime::now() 158 | .duration_since(UNIX_EPOCH) 159 | .unwrap() 160 | .as_millis() 161 | .to_string(); 162 | 163 | self.wallet 164 | .withdraw(binance::rest_model::CoinWithdrawalQuery { 165 | coin: token.to_string(), 166 | network: Some("SOL".into()), 167 | withdraw_order_id: Some(withdraw_order_id.clone()), 168 | address: address.to_string(), 169 | amount, 170 | ..binance::rest_model::CoinWithdrawalQuery::default() 171 | }) 172 | .await?; 173 | 174 | Ok((withdraw_order_id, withdraw_fee)) 175 | } 176 | 177 | async fn balances( 178 | &self, 179 | ) -> Result, Box> { 180 | let account = self.account.get_account().await?; 181 | 182 | let mut balances = HashMap::new(); 183 | for coin in ["SOL"].iter().chain(USD_COINS) { 184 | if let Some(balance) = account.balances.iter().find(|b| b.asset == *coin) { 185 | let available = balance.free; 186 | let total = available + balance.locked; 187 | 188 | balances.insert(coin.to_string(), ExchangeBalance { available, total }); 189 | } 190 | } 191 | 192 | Ok(balances) 193 | } 194 | 195 | async fn print_market_info( 196 | &self, 197 | pair: &str, 198 | format: MarketInfoFormat, 199 | ) -> Result<(), Box> { 200 | let ticker_price = self.market.get_24h_price_stats(pair).await?; 201 | 202 | match format { 203 | MarketInfoFormat::All => { 204 | println!("Pair: {pair}"); 205 | println!( 206 | "Ask: ${}, Bid: ${}, High: ${}, Low: ${}, ", 207 | ticker_price.ask_price, 208 | ticker_price.bid_price, 209 | ticker_price.high_price, 210 | ticker_price.low_price 211 | ); 212 | 213 | let average_price = self.market.get_average_price(pair).await?; 214 | 215 | println!( 216 | "Last {} minute average: ${}", 217 | average_price.mins, average_price.price 218 | ); 219 | println!( 220 | "Last 24h change: ${} ({}%)", 221 | ticker_price.price_change, ticker_price.price_change_percent 222 | ); 223 | println!( 224 | "Weighted 24h average price: ${}", 225 | ticker_price.weighted_avg_price 226 | ); 227 | } 228 | MarketInfoFormat::Ask => { 229 | println!("{}", ticker_price.ask_price); 230 | } 231 | MarketInfoFormat::Weighted24hAveragePrice => { 232 | println!("{}", ticker_price.weighted_avg_price); 233 | } 234 | MarketInfoFormat::Hourly => { 235 | return Err("Hourly market info currently supported for Binance".into()) 236 | } 237 | } 238 | Ok(()) 239 | } 240 | 241 | async fn bid_ask(&self, pair: &str) -> Result> { 242 | let binance::rest_model::PriceStats { 243 | ask_price, 244 | bid_price, 245 | .. 246 | } = self.market.get_24h_price_stats(pair).await?; 247 | 248 | Ok(BidAsk { 249 | bid_price, 250 | ask_price, 251 | }) 252 | } 253 | 254 | async fn place_order( 255 | &self, 256 | pair: &str, 257 | side: OrderSide, 258 | price: f64, 259 | amount: f64, 260 | ) -> Result> { 261 | // Minimum notional value for orders is $10 USD 262 | if price * amount < 10. { 263 | return Err("Total order amount must be 10 or greater".into()); 264 | } 265 | 266 | Ok(self 267 | .account 268 | .place_order(binance::account::OrderRequest { 269 | symbol: pair.into(), 270 | side: match side { 271 | OrderSide::Buy => binance::rest_model::OrderSide::Buy, 272 | OrderSide::Sell => binance::rest_model::OrderSide::Sell, 273 | }, 274 | order_type: binance::rest_model::OrderType::LimitMaker, 275 | price: Some(price), 276 | quantity: Some(amount), 277 | new_order_resp_type: Some(binance::rest_model::OrderResponse::Full), 278 | ..binance::account::OrderRequest::default() 279 | }) 280 | .await? 281 | .client_order_id) 282 | } 283 | 284 | async fn cancel_order( 285 | &self, 286 | pair: &str, 287 | order_id: &OrderId, 288 | ) -> Result<(), Box> { 289 | self.account 290 | .cancel_order(binance::account::OrderCancellation { 291 | symbol: pair.into(), 292 | order_id: None, 293 | orig_client_order_id: Some(order_id.into()), 294 | new_client_order_id: None, 295 | recv_window: None, 296 | }) 297 | .await?; 298 | 299 | Ok(()) 300 | } 301 | 302 | async fn order_status( 303 | &self, 304 | pair: &str, 305 | order_id: &OrderId, 306 | ) -> Result> { 307 | let order = self 308 | .account 309 | .order_status(binance::account::OrderStatusRequest { 310 | symbol: pair.into(), 311 | orig_client_order_id: Some(order_id.into()), 312 | ..binance::account::OrderStatusRequest::default() 313 | }) 314 | .await?; 315 | 316 | assert_eq!(order.order_type, binance::rest_model::OrderType::LimitMaker); 317 | assert_eq!(order.time_in_force, binance::rest_model::TimeInForce::GTC); 318 | assert_eq!(&order.symbol, pair); 319 | assert_eq!(order.client_order_id, *order_id); 320 | 321 | let last_update = Local 322 | .timestamp_opt((order.update_time / 1000) as i64, 0) 323 | .unwrap() 324 | .date_naive(); 325 | 326 | let side = match order.side { 327 | binance::rest_model::OrderSide::Sell => OrderSide::Sell, 328 | binance::rest_model::OrderSide::Buy => OrderSide::Buy, 329 | }; 330 | 331 | let trade_fees = self.wallet.trade_fees(Some(pair.to_string())).await?; 332 | 333 | let fee = trade_fees.first().map(|trade_fee| { 334 | assert_eq!(&trade_fee.symbol, pair); 335 | (trade_fee.maker_commission * order.executed_qty, { 336 | // TODO: Avoid hard code and support pairs generically... 337 | assert!(matches!(trade_fee.symbol.as_str(), "SOLUSD" | "SOLBUSD")); 338 | if side == OrderSide::Sell { 339 | "USD".into() 340 | } else { 341 | "SOL".into() 342 | } 343 | }) 344 | }); 345 | 346 | Ok(OrderStatus { 347 | open: matches!( 348 | order.status, 349 | binance::rest_model::OrderStatus::New 350 | | binance::rest_model::OrderStatus::PartiallyFilled 351 | ), 352 | side, 353 | price: order.price, 354 | amount: order.orig_qty, 355 | filled_amount: order.executed_qty, 356 | last_update, 357 | fee, 358 | }) 359 | } 360 | 361 | async fn get_lending_info( 362 | &self, 363 | _coin: &str, 364 | ) -> Result, Box> { 365 | Err("Lending not currently supported for Binance".into()) 366 | } 367 | 368 | async fn get_lending_history( 369 | &self, 370 | _lending_history: LendingHistory, 371 | ) -> Result, Box> { 372 | Err("Lending not currently supported for Binance".into()) 373 | } 374 | 375 | async fn submit_lending_offer( 376 | &self, 377 | _coin: &str, 378 | _size: f64, 379 | ) -> Result<(), Box> { 380 | Err("Lending not currently supported for Binance".into()) 381 | } 382 | 383 | fn preferred_solusd_pair(&self) -> &'static str { 384 | self.preferred_solusd_pair 385 | } 386 | } 387 | 388 | fn _new( 389 | ExchangeCredentials { 390 | api_key, 391 | secret, 392 | subaccount, 393 | }: ExchangeCredentials, 394 | binance_us: bool, 395 | ) -> Result> { 396 | if subaccount.is_some() { 397 | return Err("subaccounts not supported".into()); 398 | } 399 | 400 | let config = binance::config::Config { 401 | rest_api_endpoint: if binance_us { 402 | "https://api.binance.us" 403 | } else { 404 | "https://api.binance.com" 405 | } 406 | .into(), 407 | binance_us_api: binance_us, 408 | ..binance::config::Config::default() 409 | }; 410 | 411 | let account = binance::api::Binance::new_with_config( 412 | Some(api_key.clone()), 413 | Some(secret.clone()), 414 | &config, 415 | ); 416 | 417 | let market = binance::api::Binance::new_with_config( 418 | Some(api_key.clone()), 419 | Some(secret.clone()), 420 | &config, 421 | ); 422 | let wallet: binance::wallet::Wallet = 423 | binance::api::Binance::new_with_config(Some(api_key), Some(secret), &config); 424 | 425 | Ok(BinanceExchangeClient { 426 | account, 427 | market, 428 | wallet, 429 | preferred_solusd_pair: if binance_us { "SOLUSD" } else { "SOLBUSD" }, 430 | }) 431 | } 432 | 433 | pub fn new( 434 | exchange_credentials: ExchangeCredentials, 435 | ) -> Result> { 436 | _new(exchange_credentials, false) 437 | } 438 | 439 | pub fn new_us( 440 | exchange_credentials: ExchangeCredentials, 441 | ) -> Result> { 442 | _new(exchange_credentials, true) 443 | } 444 | -------------------------------------------------------------------------------- /src/token.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::coin_gecko, 3 | chrono::prelude::*, 4 | rust_decimal::prelude::*, 5 | separator::FixedPlaceSeparatable, 6 | serde::{Deserialize, Serialize}, 7 | solana_client::rpc_client::RpcClient, 8 | solana_sdk::{ 9 | native_token::{lamports_to_sol, sol_to_lamports}, 10 | pubkey, 11 | pubkey::Pubkey, 12 | }, 13 | std::str::FromStr, 14 | strum::{EnumString, IntoStaticStr}, 15 | }; 16 | 17 | #[derive( 18 | Debug, 19 | PartialEq, 20 | Eq, 21 | Hash, 22 | Copy, 23 | Clone, 24 | Serialize, 25 | Deserialize, 26 | EnumString, 27 | IntoStaticStr, 28 | PartialOrd, 29 | Ord, 30 | )] 31 | #[allow(clippy::upper_case_acronyms)] 32 | #[allow(non_camel_case_types)] 33 | pub enum Token { 34 | USDC, 35 | USDS, 36 | USDT, 37 | UXD, 38 | bSOL, 39 | hSOL, 40 | mSOL, 41 | stSOL, 42 | JitoSOL, 43 | tuSOL, 44 | tuUSDC, 45 | tumSOL, 46 | tustSOL, 47 | wSOL, 48 | JLP, 49 | JUP, 50 | JTO, 51 | BONK, 52 | KMNO, 53 | PYTH, 54 | WEN, 55 | WIF, 56 | PYUSD, 57 | } 58 | 59 | impl Token { 60 | pub fn mint(&self) -> Pubkey { 61 | match self { 62 | Token::USDC => pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), 63 | Token::USDS => pubkey!("USDSwr9ApdHk5bvJKMjzff41FfuX8bSxdKcR81vTwcA"), 64 | Token::USDT => pubkey!("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), 65 | Token::UXD => pubkey!("7kbnvuGBxxj8AG9qp8Scn56muWGaRaFqxg1FsRp3PaFT"), 66 | Token::tuUSDC => pubkey!("Amig8TisuLpzun8XyGfC5HJHHGUQEscjLgoTWsCCKihg"), 67 | Token::bSOL => pubkey!("bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1"), 68 | Token::hSOL => pubkey!("he1iusmfkpAdwvxLNGV8Y1iSbj4rUy6yMhEA3fotn9A"), 69 | Token::mSOL => pubkey!("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So"), 70 | Token::stSOL => pubkey!("7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj"), 71 | Token::JitoSOL => pubkey!("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn"), 72 | Token::tuSOL => pubkey!("H4Q3hDbuMUw8Bu72Ph8oV2xMQ7BFNbekpfQZKS2xF7jW"), 73 | Token::tumSOL => pubkey!("8cn7JcYVjDZesLa3RTt3NXne4WcDw9PdUneQWuByehwW"), 74 | Token::tustSOL => pubkey!("27CaAiuFW3EwLcTCaiBnexqm5pxht845AHgSuq36byKX"), 75 | Token::wSOL => spl_token::native_mint::id(), 76 | Token::JLP => pubkey!("27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4"), 77 | Token::JUP => pubkey!("JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"), 78 | Token::JTO => pubkey!("jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL"), 79 | Token::BONK => pubkey!("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"), 80 | Token::KMNO => pubkey!("KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS"), 81 | Token::PYTH => pubkey!("HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3"), 82 | Token::WEN => pubkey!("WENWENvqqNya429ubCdR81ZmD69brwQaaBYY6p3LCpk"), 83 | Token::WIF => pubkey!("EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm"), 84 | Token::PYUSD => pubkey!("2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo"), 85 | } 86 | } 87 | 88 | pub fn program_id(&self) -> Pubkey { 89 | match self { 90 | Token::USDC 91 | | Token::USDS 92 | | Token::USDT 93 | | Token::UXD 94 | | Token::tuUSDC 95 | | Token::bSOL 96 | | Token::hSOL 97 | | Token::mSOL 98 | | Token::stSOL 99 | | Token::JitoSOL 100 | | Token::tuSOL 101 | | Token::tumSOL 102 | | Token::tustSOL 103 | | Token::wSOL 104 | | Token::JLP 105 | | Token::JUP 106 | | Token::JTO 107 | | Token::BONK 108 | | Token::KMNO 109 | | Token::PYTH 110 | | Token::WEN 111 | | Token::WIF => spl_token::id(), 112 | Token::PYUSD => spl_token_2022::id(), 113 | } 114 | } 115 | pub fn ata(&self, wallet_address: &Pubkey) -> Pubkey { 116 | spl_associated_token_account::get_associated_token_address_with_program_id( 117 | wallet_address, 118 | &self.mint(), 119 | &self.program_id(), 120 | ) 121 | } 122 | 123 | pub fn symbol(&self) -> &'static str { 124 | match self { 125 | Token::USDC => "($)", 126 | Token::USDS => "USDS$", 127 | Token::USDT => "USDT$", 128 | Token::UXD => "UXD$", 129 | Token::tuUSDC => "tu($)", 130 | Token::bSOL => "b◎", 131 | Token::hSOL => "h◎", 132 | Token::mSOL => "m◎", 133 | Token::stSOL => "st◎", 134 | Token::JitoSOL => "jito◎", 135 | Token::tuSOL => "tu◎", 136 | Token::tumSOL => "tum◎", 137 | Token::tustSOL => "tust◎", 138 | Token::wSOL => "(◎)", 139 | Token::JLP => "JLP/", 140 | Token::JUP => "JUP/", 141 | Token::JTO => "JTO/", 142 | Token::BONK => "!", 143 | Token::KMNO => "KMNO/", 144 | Token::PYTH => "PYTH/", 145 | Token::WEN => "WEN/", 146 | Token::WIF => "WIF/", 147 | Token::PYUSD => "PY($)/", 148 | } 149 | } 150 | 151 | pub fn decimals(&self) -> u8 { 152 | match self { 153 | Token::BONK | Token::WEN => 5, 154 | Token::USDC 155 | | Token::USDS 156 | | Token::USDT 157 | | Token::UXD 158 | | Token::tuUSDC 159 | | Token::JLP 160 | | Token::JUP 161 | | Token::KMNO 162 | | Token::PYTH 163 | | Token::WIF => 6, 164 | Token::PYUSD => 6, 165 | Token::stSOL 166 | | Token::tuSOL 167 | | Token::bSOL 168 | | Token::hSOL 169 | | Token::mSOL 170 | | Token::JitoSOL 171 | | Token::tumSOL 172 | | Token::tustSOL 173 | | Token::JTO 174 | | Token::wSOL => 9, 175 | } 176 | } 177 | 178 | pub fn ui_amount(&self, amount: u64) -> f64 { 179 | spl_token::amount_to_ui_amount(amount, self.decimals()) 180 | } 181 | 182 | pub fn amount(&self, ui_amount: f64) -> u64 { 183 | spl_token::ui_amount_to_amount(ui_amount, self.decimals()) 184 | } 185 | 186 | pub fn name(&self) -> &'static str { 187 | self.into() 188 | } 189 | 190 | pub fn fiat_fungible(&self) -> bool { 191 | // Treat USDC as fully fungible for USD. It can always be redeemed 192 | // for exactly $1 from Coinbase and Circle 193 | *self == Self::USDC 194 | } 195 | 196 | pub fn liquidity_token(&self) -> Option { 197 | None 198 | /* 199 | match self { 200 | Token::USDC 201 | | Token::USDT 202 | | Token::UXD 203 | | Token::bSOL 204 | | Token::mSOL 205 | | Token::stSOL 206 | | Token::JitoSOL 207 | | Token::wSOL 208 | | Token::JLP => None, 209 | | Token::JUP => None, 210 | Token::tuUSDC | Token::tuSOL | Token::tumSOL | Token::tustSOL => { 211 | None 212 | // Some(crate::tulip::liquidity_token(self)) 213 | } 214 | } 215 | */ 216 | } 217 | 218 | pub async fn get_current_liquidity_token_rate( 219 | &self, 220 | _rpc_client: &RpcClient, 221 | ) -> Result> { 222 | unreachable!() 223 | /* 224 | match self { 225 | Token::USDC 226 | | Token::USDT 227 | | Token::UXD 228 | | Token::bSOL 229 | | Token::mSOL 230 | | Token::stSOL 231 | | Token::JitoSOL 232 | | Token::wSOL 233 | | Token::JLP => { 234 | unreachable!() 235 | } 236 | Token::tuUSDC | Token::tuSOL | Token::tumSOL | Token::tustSOL => { 237 | unreachable!() 238 | //crate::tulip::get_current_liquidity_token_rate(rpc_client, self).await 239 | } 240 | } 241 | */ 242 | } 243 | 244 | pub fn balance( 245 | &self, 246 | rpc_client: &RpcClient, 247 | address: &Pubkey, 248 | ) -> Result> { 249 | Ok(u64::from_str( 250 | &rpc_client 251 | .get_token_account_balance(&self.ata(address)) 252 | .map_err(|_| { 253 | format!( 254 | "Could not get balance for account {}, token {}", 255 | address, 256 | self.name(), 257 | ) 258 | })? 259 | .amount, 260 | ) 261 | .unwrap_or_default()) 262 | } 263 | 264 | #[async_recursion::async_recursion(?Send)] 265 | pub async fn get_current_price( 266 | &self, 267 | _rpc_client: &RpcClient, 268 | ) -> Result> { 269 | if self.fiat_fungible() { 270 | return Ok(Decimal::from_f64(1.).unwrap()); 271 | } 272 | match self { 273 | Token::USDC 274 | | Token::USDS 275 | | Token::USDT 276 | | Token::UXD 277 | | Token::bSOL 278 | | Token::hSOL 279 | | Token::mSOL 280 | | Token::stSOL 281 | | Token::JitoSOL 282 | | Token::wSOL 283 | | Token::JLP 284 | | Token::JUP 285 | | Token::JTO 286 | | Token::BONK 287 | | Token::KMNO 288 | | Token::PYTH 289 | | Token::WEN 290 | | Token::WIF 291 | | Token::PYUSD => coin_gecko::get_current_price(&MaybeToken(Some(*self))).await, 292 | Token::tuUSDC | Token::tuSOL | Token::tumSOL | Token::tustSOL => { 293 | Err("tulip support disabled".into()) 294 | //crate::tulip::get_current_price(rpc_client, self).await 295 | } 296 | } 297 | } 298 | 299 | pub async fn get_historical_price( 300 | &self, 301 | _rpc_client: &RpcClient, 302 | when: NaiveDate, 303 | ) -> Result> { 304 | if self.fiat_fungible() { 305 | return Ok(Decimal::from_f64(1.).unwrap()); 306 | } 307 | match self { 308 | Token::USDC | Token::PYUSD => { 309 | coin_gecko::get_historical_price(when, &MaybeToken(Some(*self))).await 310 | } 311 | unsupported_token => Err(format!( 312 | "Historical price data is not available for {}", 313 | unsupported_token.name() 314 | ) 315 | .into()), 316 | } 317 | } 318 | 319 | pub fn format_amount(&self, amount: u64) -> String { 320 | self.format_ui_amount(self.ui_amount(amount)) 321 | } 322 | 323 | pub fn format_ui_amount(&self, ui_amount: f64) -> String { 324 | format!( 325 | "{}{}", 326 | self.symbol(), 327 | ui_amount.separated_string_with_fixed_place(2) 328 | ) 329 | } 330 | } 331 | 332 | pub fn is_valid_token_or_sol(value: String) -> Result<(), String> { 333 | if value == "SOL" { 334 | Ok(()) 335 | } else { 336 | is_valid_token(value) 337 | } 338 | } 339 | 340 | pub fn is_valid_token(value: String) -> Result<(), String> { 341 | Token::from_str(&value) 342 | .map(|_| ()) 343 | .map_err(|_| format!("Invalid token {value}")) 344 | } 345 | 346 | #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize, Ord, PartialOrd)] 347 | #[repr(transparent)] 348 | pub struct MaybeToken(Option); 349 | 350 | impl MaybeToken { 351 | #[allow(non_snake_case)] 352 | pub fn SOL() -> Self { 353 | Self(None) 354 | } 355 | 356 | pub fn token(&self) -> Option { 357 | self.0 358 | } 359 | 360 | pub fn is_token(&self) -> bool { 361 | self.token().is_some() 362 | } 363 | 364 | pub fn is_sol(&self) -> bool { 365 | !self.is_token() 366 | } 367 | 368 | pub fn is_sol_or_wsol(&self) -> bool { 369 | self.is_sol() || self.token() == Some(Token::wSOL) 370 | } 371 | 372 | pub fn ui_amount(&self, amount: u64) -> f64 { 373 | match self.0 { 374 | None => lamports_to_sol(amount), 375 | Some(token) => token.ui_amount(amount), 376 | } 377 | } 378 | 379 | pub fn mint(&self) -> Pubkey { 380 | match self.0 { 381 | None => spl_token::native_mint::id(), 382 | Some(token) => token.mint(), 383 | } 384 | } 385 | 386 | pub fn amount(&self, ui_amount: f64) -> u64 { 387 | match self.0 { 388 | None => sol_to_lamports(ui_amount), 389 | Some(token) => token.amount(ui_amount), 390 | } 391 | } 392 | 393 | pub fn symbol(&self) -> &'static str { 394 | match self.0 { 395 | None => "◎", 396 | Some(token) => token.symbol(), 397 | } 398 | } 399 | 400 | pub fn liquidity_token(&self) -> Option { 401 | match self.0 { 402 | None => None, 403 | Some(token) => token.liquidity_token(), 404 | } 405 | } 406 | 407 | pub async fn get_current_liquidity_token_rate( 408 | &self, 409 | rpc_client: &RpcClient, 410 | ) -> Result> { 411 | match self.0 { 412 | None => Ok(Decimal::from_usize(1).unwrap()), 413 | Some(token) => token.get_current_liquidity_token_rate(rpc_client).await, 414 | } 415 | } 416 | 417 | pub fn name(&self) -> &'static str { 418 | match self.0 { 419 | None => "SOL", 420 | Some(token) => token.into(), 421 | } 422 | } 423 | 424 | pub fn fiat_fungible(&self) -> bool { 425 | match self.0 { 426 | None => false, 427 | Some(token) => token.fiat_fungible(), 428 | } 429 | } 430 | 431 | pub fn balance( 432 | &self, 433 | rpc_client: &RpcClient, 434 | address: &Pubkey, 435 | ) -> Result> { 436 | match self.0 { 437 | None => Ok(rpc_client 438 | .get_account_with_commitment(address, rpc_client.commitment())? 439 | .value 440 | .map(|account| account.lamports) 441 | .unwrap_or_default()), 442 | Some(token) => token.balance(rpc_client, address), 443 | } 444 | } 445 | 446 | pub async fn get_current_price( 447 | &self, 448 | rpc_client: &RpcClient, 449 | ) -> Result> { 450 | match self.0 { 451 | None => coin_gecko::get_current_price(self).await, 452 | Some(token) => token.get_current_price(rpc_client).await, 453 | } 454 | } 455 | 456 | pub async fn get_historical_price( 457 | &self, 458 | rpc_client: &RpcClient, 459 | when: NaiveDate, 460 | ) -> Result> { 461 | match self.0 { 462 | None => coin_gecko::get_historical_price(when, self).await, 463 | Some(token) => token.get_historical_price(rpc_client, when).await, 464 | } 465 | } 466 | 467 | pub fn format_amount(&self, amount: u64) -> String { 468 | self.format_ui_amount(self.ui_amount(amount)) 469 | } 470 | 471 | pub fn format_ui_amount(&self, ui_amount: f64) -> String { 472 | format!( 473 | "{}{}", 474 | self.symbol(), 475 | ui_amount.separated_string_with_fixed_place(6) 476 | ) 477 | } 478 | } 479 | 480 | impl From> for MaybeToken { 481 | fn from(maybe_token: Option) -> Self { 482 | Self(maybe_token) 483 | } 484 | } 485 | 486 | impl From for MaybeToken { 487 | fn from(token: Token) -> Self { 488 | Self(Some(token)) 489 | } 490 | } 491 | 492 | impl std::fmt::Display for MaybeToken { 493 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 494 | write!(f, "{}", self.name()) 495 | } 496 | } 497 | 498 | impl std::fmt::Display for Token { 499 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 500 | write!(f, "{}", self.name()) 501 | } 502 | } 503 | --------------------------------------------------------------------------------