diff --git a/Cargo.lock b/Cargo.lock index cd36b27..1521724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2616,6 +2616,8 @@ dependencies = [ "anchor-client", "anchor-lang 0.28.0 (registry+https://github.com/rust-lang/crates.io-index)", "anchor-spl 0.28.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "bytemuck", "fixed", "mango-v4", "solana-account-decoder", @@ -2626,6 +2628,7 @@ dependencies = [ "switchboard-common", "switchboard-program", "switchboard-solana", + "tokio", ] [[package]] @@ -2959,6 +2962,7 @@ dependencies = [ "serum_dex", "solana-address-lookup-table-program", "solana-program", + "solana-sdk", "solana-security-txt", "static_assertions", "switchboard-program", diff --git a/Cargo.toml b/Cargo.toml index ddb8235..49771e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,10 @@ edition = "2021" [dependencies] +tokio = { version = "1", features = ["full"] } +anyhow = "1.0" +bytemuck = { version = "^1.7.2", features = ["min_const_generics"] } + solana-rpc = "~1.16.7" solana-client = "~1.16.7" solana-account-decoder = "~1.16.7" @@ -17,7 +21,7 @@ anchor-spl = "0.28.0" anchor-lang = "0.28.0" anchor-client = "0.28.0" -mango-v4 = { git = "https://github.com/blockworks-foundation/mango-v4.git", branch = "update-solana-1-16-anchor-28", features = ["cpi"] } +mango-v4 = { git = "https://github.com/blockworks-foundation/mango-v4.git", branch = "update-solana-1-16-anchor-28", features = ["client"] } fixed = { git = "https://github.com/blockworks-foundation/fixed.git", branch = "v1.11.0-borsh0_10-mango" } switchboard-program = "0.2.0" diff --git a/src/main.rs b/src/main.rs index 7cf3ebf..abdbe5b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,125 @@ -fn main() { - println!("Hello, world!"); +use anchor_lang::prelude::Pubkey; +use anyhow::Error; +use mango_v4::state::{Bank, MangoAccountValue}; +use solana_account_decoder::UiAccountEncoding; +use solana_client::rpc_config::RpcProgramAccountsConfig; +use solana_client::rpc_filter::{Memcmp, RpcFilterType}; +use solana_client::{ + nonblocking::rpc_client::RpcClient as RpcClientAsync, rpc_config::RpcAccountInfoConfig, +}; +use solana_sdk::pubkey; - // get JITOSOL value of a mango account +const JITOSOL_TOKEN_INDEX: u16 = 501; - // get +pub async fn fetch_mango_accounts_by_owner( + rpc: &RpcClientAsync, + program: Pubkey, + group: Pubkey, + owner: Pubkey, +) -> anyhow::Result> { + let config = RpcProgramAccountsConfig { + filters: Some(vec![ + RpcFilterType::Memcmp(Memcmp::new_raw_bytes( + 0, + [243, 228, 247, 3, 169, 52, 175, 31].to_vec(), // mango discriminator + )), + RpcFilterType::Memcmp(Memcmp::new_raw_bytes(8, group.to_bytes().to_vec())), + RpcFilterType::Memcmp(Memcmp::new_raw_bytes(40, owner.to_bytes().to_vec())), + ]), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + ..RpcAccountInfoConfig::default() + }, + ..RpcProgramAccountsConfig::default() + }; + rpc.get_program_accounts_with_config(&program, config) + .await? + .into_iter() + .map(|(key, account)| Ok((key, MangoAccountValue::from_bytes(&account.data[8..])?))) + .collect::, _>>() +} + +async fn fetch_banks( + rpc: &RpcClientAsync, + program: Pubkey, + group: Pubkey, +) -> anyhow::Result> { + let filters = vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes( + 8, + group.to_bytes().to_vec(), + ))]; + let account_type_filter = RpcFilterType::Memcmp(Memcmp::new_raw_bytes( + 0, + [142, 49, 166, 242, 50, 66, 97, 188].to_vec(), // bank discriminator + )); + let config = RpcProgramAccountsConfig { + filters: Some([vec![account_type_filter], filters].concat()), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + ..RpcAccountInfoConfig::default() + }, + ..RpcProgramAccountsConfig::default() + }; + rpc.get_program_accounts_with_config(&program, config) + .await? + .into_iter() + .map(|(key, account)| Ok((key, *bytemuck::from_bytes::(&(&account.data[8..]))))) + .collect() +} + +pub async fn fetch_jitosol_bank( + rpc: &RpcClientAsync, + program: Pubkey, + group: Pubkey, +) -> anyhow::Result { + let token_banks = fetch_banks(&rpc, program, group).await.unwrap(); + match token_banks + .iter() + .find(|(_, b)| b.token_index == JITOSOL_TOKEN_INDEX) + { + Some(jb) => Ok(jb.1), + None => Err(Error::msg("JitoSol token bank not found")), + } +} + +pub async fn fetch_jitosol_exposure( + rpc: &RpcClientAsync, + program: Pubkey, + group: Pubkey, + owner_pk: Pubkey, + jito_bank: Bank, +) -> anyhow::Result { + let mut jitosol_amount = 0f64; + + let mango_account_tuples = fetch_mango_accounts_by_owner(&rpc, program, group, owner_pk) + .await + .unwrap(); + for (_, acct) in mango_account_tuples.iter() { + match acct.token_position(JITOSOL_TOKEN_INDEX) { + Ok(token_position) => { + // token_position.ui is positive in the case of deposits and negative in the case of borrows + jitosol_amount += token_position.ui(&jito_bank).to_num::().abs() + } + Err(_) => continue, + } + } + Ok(jitosol_amount) +} + +#[tokio::main] +async fn main() { + let program = mango_v4::ID; + let group = pubkey!("78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX"); + let owner_pk = pubkey!("Wallet Private Key Here"); + let rpc = + RpcClientAsync::new("RPC HERE".to_string()); + + let jito_bank = fetch_jitosol_bank(&rpc, program, group).await.unwrap(); + let jitosol_exposure = fetch_jitosol_exposure(&rpc, program, group, owner_pk, jito_bank) + .await + .unwrap(); + println!( + "Mango accounts owned by {:?} have {:?} of JitoSol exposure", + owner_pk, jitosol_exposure + ); }