diff --git a/Cargo.lock b/Cargo.lock index 7414c55d3..c81d48db7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1118,10 +1118,12 @@ dependencies = [ "anchor-lang", "anchor-spl", "anyhow", + "async-trait", "base64 0.13.1", "bincode", "fixed", "fixed-macro", + "futures 0.3.25", "itertools 0.10.5", "log 0.4.17", "mango-v4", @@ -1132,6 +1134,7 @@ dependencies = [ "serum_dex 0.5.6", "shellexpand", "solana-account-decoder", + "solana-address-lookup-table-program", "solana-client", "solana-sdk", "spl-associated-token-account", diff --git a/cli/src/main.rs b/cli/src/main.rs index f214caa61..d52488d6d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -119,7 +119,8 @@ impl Rpc { } } -fn main() -> Result<(), anyhow::Error> { +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { env_logger::init_from_env( env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), ); @@ -137,7 +138,7 @@ fn main() -> Result<(), anyhow::Error> { num } else { // find free account_num - let accounts = MangoClient::find_accounts(&client, group, &owner)?; + let accounts = MangoClient::find_accounts(&client, group, &owner).await?; if accounts.is_empty() { 0 } else { @@ -149,14 +150,9 @@ fn main() -> Result<(), anyhow::Error> { + 1 } }; - let (account, txsig) = MangoClient::create_account( - &client, - group, - &owner, - &owner, - account_num, - &cmd.name, - )?; + let (account, txsig) = + MangoClient::create_account(&client, group, &owner, &owner, account_num, &cmd.name) + .await?; println!("{}", account); println!("{}", txsig); } @@ -165,8 +161,8 @@ fn main() -> Result<(), anyhow::Error> { let account = client::pubkey_from_cli(&cmd.account); let owner = client::keypair_from_cli(&cmd.owner); let mint = client::pubkey_from_cli(&cmd.mint); - let client = MangoClient::new_for_existing_account(client, account, owner)?; - let txsig = client.token_deposit(mint, cmd.amount)?; + let client = MangoClient::new_for_existing_account(client, account, owner).await?; + let txsig = client.token_deposit(mint, cmd.amount).await?; println!("{}", txsig); } Command::JupiterSwap(cmd) => { @@ -175,14 +171,16 @@ fn main() -> Result<(), anyhow::Error> { let owner = client::keypair_from_cli(&cmd.owner); let input_mint = client::pubkey_from_cli(&cmd.input_mint); let output_mint = client::pubkey_from_cli(&cmd.output_mint); - let client = MangoClient::new_for_existing_account(client, account, owner)?; - let txsig = client.jupiter_swap( - input_mint, - output_mint, - cmd.amount, - cmd.slippage, - client::JupiterSwapMode::ExactIn, - )?; + let client = MangoClient::new_for_existing_account(client, account, owner).await?; + let txsig = client + .jupiter_swap( + input_mint, + output_mint, + cmd.amount, + cmd.slippage, + client::JupiterSwapMode::ExactIn, + ) + .await?; println!("{}", txsig); } Command::GroupAddress { creator, num } => { diff --git a/client/Cargo.toml b/client/Cargo.toml index 901913398..a9c1274b8 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -11,8 +11,10 @@ anchor-client = { path = "../anchor/client" } anchor-lang = { path = "../anchor/lang" } anchor-spl = { path = "../anchor/spl" } anyhow = "1.0" +async-trait = "0.1.52" fixed = { version = "=1.11.0", features = ["serde", "borsh"] } fixed-macro = "^1.1.1" +futures = "0.3.25" itertools = "0.10.3" mango-v4 = { path = "../programs/mango-v4", features = ["no-entrypoint", "client"] } pyth-sdk-solana = "0.1.0" @@ -21,6 +23,7 @@ shellexpand = "2.1.0" solana-account-decoder = "~1.14.9" solana-client = "~1.14.9" solana-sdk = "~1.14.9" +solana-address-lookup-table-program = "~1.14.9" spl-associated-token-account = "1.0.3" thiserror = "1.0.31" log = "0.4" @@ -29,4 +32,4 @@ tokio = { version = "1", features = ["full"] } serde = "1.0.141" serde_json = "1.0.82" base64 = "0.13.0" -bincode = "1.3.3" \ No newline at end of file +bincode = "1.3.3" diff --git a/client/src/account_fetcher.rs b/client/src/account_fetcher.rs index a63d203cd..e29b73dc6 100644 --- a/client/src/account_fetcher.rs +++ b/client/src/account_fetcher.rs @@ -6,15 +6,22 @@ use anyhow::Context; use anchor_client::ClientError; use anchor_lang::AccountDeserialize; -use solana_client::rpc_client::RpcClient; +use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_sdk::account::{AccountSharedData, ReadableAccount}; use solana_sdk::pubkey::Pubkey; use mango_v4::state::MangoAccountValue; +#[async_trait::async_trait(?Send)] pub trait AccountFetcher: Sync + Send { - fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result; - fn fetch_program_accounts( + async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result; + async fn fetch_raw_account_lookup_table( + &self, + address: &Pubkey, + ) -> anyhow::Result { + self.fetch_raw_account(address).await + } + async fn fetch_program_accounts( &self, program: &Pubkey, discriminator: [u8; 8], @@ -22,35 +29,37 @@ pub trait AccountFetcher: Sync + Send { } // Can't be in the trait, since then it would no longer be object-safe... -pub fn account_fetcher_fetch_anchor_account( +pub async fn account_fetcher_fetch_anchor_account( fetcher: &dyn AccountFetcher, address: &Pubkey, ) -> anyhow::Result { - let account = fetcher.fetch_raw_account(address)?; + let account = fetcher.fetch_raw_account(address).await?; let mut data: &[u8] = &account.data(); T::try_deserialize(&mut data) .with_context(|| format!("deserializing anchor account {}", address)) } // Can't be in the trait, since then it would no longer be object-safe... -pub fn account_fetcher_fetch_mango_account( +pub async fn account_fetcher_fetch_mango_account( fetcher: &dyn AccountFetcher, address: &Pubkey, ) -> anyhow::Result { - let account = fetcher.fetch_raw_account(address)?; + let account = fetcher.fetch_raw_account(address).await?; let data: &[u8] = &account.data(); MangoAccountValue::from_bytes(&data[8..]) .with_context(|| format!("deserializing mango account {}", address)) } pub struct RpcAccountFetcher { - pub rpc: RpcClient, + pub rpc: RpcClientAsync, } +#[async_trait::async_trait(?Send)] impl AccountFetcher for RpcAccountFetcher { - fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result { + async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result { self.rpc .get_account_with_commitment(address, self.rpc.commitment()) + .await .with_context(|| format!("fetch account {}", *address))? .value .ok_or(ClientError::AccountNotFound) @@ -58,7 +67,7 @@ impl AccountFetcher for RpcAccountFetcher { .map(Into::into) } - fn fetch_program_accounts( + async fn fetch_program_accounts( &self, program: &Pubkey, discriminator: [u8; 8], @@ -78,7 +87,10 @@ impl AccountFetcher for RpcAccountFetcher { }, with_context: Some(true), }; - let accs = self.rpc.get_program_accounts_with_config(program, config)?; + let accs = self + .rpc + .get_program_accounts_with_config(program, config) + .await?; // convert Account -> AccountSharedData Ok(accs .into_iter() @@ -121,18 +133,19 @@ impl CachedAccountFetcher { } } +#[async_trait::async_trait(?Send)] impl AccountFetcher for CachedAccountFetcher { - fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result { + async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result { let mut cache = self.cache.lock().unwrap(); if let Some(account) = cache.accounts.get(address) { return Ok(account.clone()); } - let account = self.fetcher.fetch_raw_account(address)?; + let account = self.fetcher.fetch_raw_account(address).await?; cache.accounts.insert(*address, account.clone()); Ok(account) } - fn fetch_program_accounts( + async fn fetch_program_accounts( &self, program: &Pubkey, discriminator: [u8; 8], @@ -147,7 +160,8 @@ impl AccountFetcher for CachedAccountFetcher { } let accounts = self .fetcher - .fetch_program_accounts(program, discriminator)?; + .fetch_program_accounts(program, discriminator) + .await?; cache .keys_for_program_and_discriminator .insert(cache_key, accounts.iter().map(|(pk, _)| *pk).collect()); diff --git a/client/src/chain_data_fetcher.rs b/client/src/chain_data_fetcher.rs index caabb322e..090c9025a 100644 --- a/client/src/chain_data_fetcher.rs +++ b/client/src/chain_data_fetcher.rs @@ -11,7 +11,7 @@ use mango_v4::state::{MangoAccount, MangoAccountValue}; use anyhow::Context; -use solana_client::rpc_client::RpcClient; +use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_sdk::account::{AccountSharedData, ReadableAccount}; use solana_sdk::clock::Slot; use solana_sdk::pubkey::Pubkey; @@ -19,7 +19,7 @@ use solana_sdk::signature::Signature; pub struct AccountFetcher { pub chain_data: Arc>, - pub rpc: RpcClient, + pub rpc: RpcClientAsync, } impl AccountFetcher { @@ -55,16 +55,19 @@ impl AccountFetcher { } // fetches via RPC, stores in ChainData, returns new version - pub fn fetch_fresh( + pub async fn fetch_fresh( &self, address: &Pubkey, ) -> anyhow::Result { - self.refresh_account_via_rpc(address)?; + self.refresh_account_via_rpc(address).await?; self.fetch(address) } - pub fn fetch_fresh_mango_account(&self, address: &Pubkey) -> anyhow::Result { - self.refresh_account_via_rpc(address)?; + pub async fn fetch_fresh_mango_account( + &self, + address: &Pubkey, + ) -> anyhow::Result { + self.refresh_account_via_rpc(address).await?; self.fetch_mango_account(address) } @@ -76,10 +79,11 @@ impl AccountFetcher { .clone()) } - pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result { + pub async fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result { let response = self .rpc .get_account_with_commitment(address, self.rpc.commitment()) + .await .with_context(|| format!("refresh account {} via rpc", address))?; let slot = response.context.slot; let account = response @@ -100,8 +104,8 @@ impl AccountFetcher { } /// Return the maximum slot reported for the processing of the signatures - pub fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result { - let statuses = self.rpc.get_signature_statuses(signatures)?.value; + pub async fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result { + let statuses = self.rpc.get_signature_statuses(signatures).await?.value; Ok(statuses .iter() .map(|status_opt| status_opt.as_ref().map(|status| status.slot).unwrap_or(0)) @@ -110,7 +114,7 @@ impl AccountFetcher { } /// Return success once all addresses have data >= min_slot - pub fn refresh_accounts_via_rpc_until_slot( + pub async fn refresh_accounts_via_rpc_until_slot( &self, addresses: &[Pubkey], min_slot: Slot, @@ -126,7 +130,7 @@ impl AccountFetcher { min_slot ); } - let data_slot = self.refresh_account_via_rpc(address)?; + let data_slot = self.refresh_account_via_rpc(address).await?; if data_slot >= min_slot { break; } @@ -137,15 +141,29 @@ impl AccountFetcher { } } +#[async_trait::async_trait(?Send)] impl crate::AccountFetcher for AccountFetcher { - fn fetch_raw_account( + async fn fetch_raw_account( &self, address: &Pubkey, ) -> anyhow::Result { - self.fetch_raw(&address) + self.fetch_raw(address) } - fn fetch_program_accounts( + async fn fetch_raw_account_lookup_table( + &self, + address: &Pubkey, + ) -> anyhow::Result { + // Fetch data via RPC if missing: the chain data updater doesn't know about all the + // lookup talbes we may need. + if let Ok(alt) = self.fetch_raw(address) { + return Ok(alt); + } + self.refresh_account_via_rpc(address).await?; + self.fetch_raw(address) + } + + async fn fetch_program_accounts( &self, program: &Pubkey, discriminator: [u8; 8], diff --git a/client/src/client.rs b/client/src/client.rs index bc8e75221..ca5606c2b 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -1,18 +1,18 @@ -use std::rc::Rc; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use anchor_client::{ClientError, Cluster, Program}; +use anchor_client::{ClientError, Cluster}; use anchor_lang::__private::bytemuck; use anchor_lang::prelude::System; -use anchor_lang::Id; +use anchor_lang::{AccountDeserialize, Id}; use anchor_spl::associated_token::get_associated_token_address; use anchor_spl::token::Token; use bincode::Options; use fixed::types::I80F48; +use futures::{stream, StreamExt, TryStreamExt}; use itertools::Itertools; use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; @@ -20,13 +20,15 @@ use mango_v4::state::{ Bank, Group, MangoAccountValue, PerpMarketIndex, Serum3MarketIndex, TokenIndex, }; +use solana_address_lookup_table_program::state::AddressLookupTable; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; -use solana_client::rpc_client::RpcClient; +use solana_sdk::address_lookup_table_account::AddressLookupTableAccount; +use solana_sdk::hash::Hash; use solana_sdk::signer::keypair; use crate::account_fetcher::*; use crate::context::{MangoGroupContext, Serum3MarketContext, TokenContext}; -use crate::gpa::fetch_mango_accounts; +use crate::gpa::{fetch_anchor_account, fetch_mango_accounts}; use crate::jupiter; use crate::util::MyClone; @@ -61,23 +63,6 @@ impl Client { } } - pub fn anchor_client(&self) -> anchor_client::Client { - anchor_client::Client::new_with_options( - self.cluster.clone(), - Rc::new((*self.fee_payer).clone()), - self.commitment, - ) - } - - pub fn rpc(&self) -> RpcClient { - let url = self.cluster.url().to_string(); - if let Some(timeout) = self.timeout.as_ref() { - RpcClient::new_with_timeout_and_commitment(url, *timeout, self.commitment) - } else { - RpcClient::new_with_commitment(url, self.commitment) - } - } - pub fn rpc_async(&self) -> RpcClientAsync { let url = self.cluster.url().to_string(); if let Some(timeout) = self.timeout.as_ref() { @@ -86,6 +71,14 @@ impl Client { RpcClientAsync::new_with_commitment(url, self.commitment) } } + + // TODO: this function here is awkward, since it (intentionally) doesn't use MangoClient::account_fetcher + pub async fn rpc_anchor_account( + &self, + address: &Pubkey, + ) -> anyhow::Result { + fetch_anchor_account(&self.rpc_async(), address).await + } } // todo: might want to integrate geyser, websockets, or simple http polling for keeping data fresh @@ -101,18 +94,7 @@ pub struct MangoClient { pub context: MangoGroupContext, - // Since MangoClient currently provides a blocking interface, we'd prefer to use reqwest::blocking::Client - // but that doesn't work inside async contexts. Hence we use the async reqwest Client instead and use - // a manual runtime to bridge into async code from both sync and async contexts. - // That doesn't work perfectly, see MangoClient::invoke(). pub http_client: reqwest::Client, - runtime: Option, -} - -impl Drop for MangoClient { - fn drop(&mut self) { - self.runtime.take().expect("runtime").shutdown_background(); - } } // TODO: add retry framework for sending tx and rpc calls @@ -129,26 +111,27 @@ impl MangoClient { .0 } - pub fn find_accounts( + pub async fn find_accounts( client: &Client, group: Pubkey, owner: &Keypair, ) -> anyhow::Result> { - let program = client.anchor_client().program(mango_v4::ID); - fetch_mango_accounts(&program, group, owner.pubkey()).map_err(Into::into) + fetch_mango_accounts(&client.rpc_async(), mango_v4::ID, group, owner.pubkey()).await } - pub fn find_or_create_account( + pub async fn find_or_create_account( client: &Client, group: Pubkey, owner: &Keypair, payer: &Keypair, // pays the SOL for the new account mango_account_name: &str, ) -> anyhow::Result { - let program = client.anchor_client().program(mango_v4::ID); + let rpc = client.rpc_async(); + let program = mango_v4::ID; // Mango Account - let mut mango_account_tuples = fetch_mango_accounts(&program, group, owner.pubkey())?; + let mut mango_account_tuples = + fetch_mango_accounts(&rpc, program, group, owner.pubkey()).await?; let mango_account_opt = mango_account_tuples .iter() .find(|(_, account)| account.fixed.name() == mango_account_name); @@ -164,9 +147,11 @@ impl MangoClient { None => 0u32, }; Self::create_account(client, group, owner, payer, account_num, mango_account_name) + .await .context("Failed to create account...")?; } - let mango_account_tuples = fetch_mango_accounts(&program, group, owner.pubkey())?; + let mango_account_tuples = + fetch_mango_accounts(&rpc, program, group, owner.pubkey()).await?; let index = mango_account_tuples .iter() .position(|tuple| tuple.1.fixed.name() == mango_account_name) @@ -174,7 +159,7 @@ impl MangoClient { Ok(mango_account_tuples[index].0) } - pub fn create_account( + pub async fn create_account( client: &Client, group: Pubkey, owner: &Keypair, @@ -182,7 +167,8 @@ impl MangoClient { account_num: u32, mango_account_name: &str, ) -> anyhow::Result<(Pubkey, Signature)> { - let program = client.anchor_client().program(mango_v4::ID); + let rpc = client.rpc_async(); + let account = Pubkey::find_program_address( &[ group.as_ref(), @@ -193,46 +179,50 @@ impl MangoClient { &mango_v4::id(), ) .0; - let txsig = program - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::AccountCreate { - group, - owner: owner.pubkey(), - account, - payer: payer.pubkey(), - system_program: System::id(), - }, - None, - ), - data: anchor_lang::InstructionData::data(&mango_v4::instruction::AccountCreate { - account_num, - name: mango_account_name.to_owned(), - token_count: 8, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - }), - }) - .signer(owner) - .signer(payer) - .send() - .map_err(prettify_client_error)?; + let ix = Instruction { + program_id: mango_v4::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::AccountCreate { + group, + owner: owner.pubkey(), + account, + payer: payer.pubkey(), + system_program: System::id(), + }, + None, + ), + data: anchor_lang::InstructionData::data(&mango_v4::instruction::AccountCreate { + account_num, + name: mango_account_name.to_owned(), + token_count: 8, + serum3_count: 8, + perp_count: 8, + perp_oo_count: 8, + }), + }; + + let txsig = TransactionBuilder { + instructions: vec![ix], + address_lookup_tables: vec![], + payer: payer.pubkey(), + signers: vec![owner, payer], + } + .send_and_confirm(&rpc) + .await?; Ok((account, txsig)) } /// Conveniently creates a RPC based client - pub fn new_for_existing_account( + pub async fn new_for_existing_account( client: Client, account: Pubkey, owner: Keypair, ) -> anyhow::Result { - let rpc = client.rpc(); + let rpc = client.rpc_async(); let account_fetcher = Arc::new(CachedAccountFetcher::new(RpcAccountFetcher { rpc })); - let mango_account = account_fetcher_fetch_mango_account(&*account_fetcher, &account)?; + let mango_account = + account_fetcher_fetch_mango_account(&*account_fetcher, &account).await?; let group = mango_account.fixed.group; if mango_account.fixed.owner != owner.pubkey() { anyhow::bail!( @@ -242,8 +232,8 @@ impl MangoClient { ); } - let group_context = - MangoGroupContext::new_from_rpc(group, client.cluster.clone(), client.commitment)?; + let rpc = client.rpc_async(); + let group_context = MangoGroupContext::new_from_rpc(&rpc, group).await?; Self::new_detail(client, account, owner, group_context, account_fetcher) } @@ -264,25 +254,9 @@ impl MangoClient { mango_account_address: account, context: group_context, http_client: reqwest::Client::new(), - runtime: Some( - tokio::runtime::Builder::new_current_thread() - .thread_name("mango-client") - .enable_io() - .enable_time() - .build() - .unwrap(), - ), }) } - pub fn anchor_client(&self) -> anchor_client::Client { - self.client.anchor_client() - } - - pub fn program(&self) -> Program { - self.anchor_client().program(mango_v4::ID) - } - pub fn owner(&self) -> Pubkey { self.owner.pubkey() } @@ -291,21 +265,22 @@ impl MangoClient { self.context.group } - pub fn mango_account(&self) -> anyhow::Result { + pub async fn mango_account(&self) -> anyhow::Result { account_fetcher_fetch_mango_account(&*self.account_fetcher, &self.mango_account_address) + .await } - pub fn first_bank(&self, token_index: TokenIndex) -> anyhow::Result { + pub async fn first_bank(&self, token_index: TokenIndex) -> anyhow::Result { let bank_address = self.context.mint_info(token_index).first_bank(); - account_fetcher_fetch_anchor_account(&*self.account_fetcher, &bank_address) + account_fetcher_fetch_anchor_account(&*self.account_fetcher, &bank_address).await } - pub fn derive_health_check_remaining_account_metas( + pub async fn derive_health_check_remaining_account_metas( &self, affected_tokens: Vec, writable_banks: bool, ) -> anyhow::Result> { - let account = self.mango_account()?; + let account = self.mango_account().await?; self.context.derive_health_check_remaining_account_metas( &account, affected_tokens, @@ -313,12 +288,12 @@ impl MangoClient { ) } - pub fn derive_liquidation_health_check_remaining_account_metas( + pub async fn derive_liquidation_health_check_remaining_account_metas( &self, liqee: &MangoAccountValue, writable_banks: &[TokenIndex], ) -> anyhow::Result> { - let account = self.mango_account()?; + let account = self.mango_account().await?; self.context .derive_health_check_remaining_account_metas_two_accounts( &account, @@ -327,49 +302,43 @@ impl MangoClient { ) } - pub fn token_deposit(&self, mint: Pubkey, amount: u64) -> anyhow::Result { + pub async fn token_deposit(&self, mint: Pubkey, amount: u64) -> anyhow::Result { let token = self.context.token_by_mint(&mint)?; let token_index = token.token_index; let mint_info = token.mint_info; - let health_check_metas = - self.derive_health_check_remaining_account_metas(vec![token_index], false)?; + let health_check_metas = self + .derive_health_check_remaining_account_metas(vec![token_index], false) + .await?; - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::TokenDeposit { - group: self.group(), - account: self.mango_account_address, - owner: self.owner(), - bank: mint_info.first_bank(), - vault: mint_info.first_vault(), - oracle: mint_info.oracle, - token_account: get_associated_token_address( - &self.owner(), - &mint_info.mint, - ), - token_authority: self.owner(), - token_program: Token::id(), - }, - None, - ); - ams.extend(health_check_metas.into_iter()); - ams - }, - data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenDeposit { - amount, - }), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error) + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::TokenDeposit { + group: self.group(), + account: self.mango_account_address, + owner: self.owner(), + bank: mint_info.first_bank(), + vault: mint_info.first_vault(), + oracle: mint_info.oracle, + token_account: get_associated_token_address(&self.owner(), &mint_info.mint), + token_authority: self.owner(), + token_program: Token::id(), + }, + None, + ); + ams.extend(health_check_metas.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenDeposit { + amount, + }), + }; + self.send_and_confirm_owner_tx(vec![ix]).await } - pub fn token_withdraw( + pub async fn token_withdraw( &self, mint: Pubkey, amount: u64, @@ -379,18 +348,18 @@ impl MangoClient { let token_index = token.token_index; let mint_info = token.mint_info; - let health_check_metas = - self.derive_health_check_remaining_account_metas(vec![token_index], false)?; + let health_check_metas = self + .derive_health_check_remaining_account_metas(vec![token_index], false) + .await?; - self.program() - .request() - .instruction(spl_associated_token_account::instruction::create_associated_token_account_idempotent( + let ixs = vec![ + spl_associated_token_account::instruction::create_associated_token_account_idempotent( &self.owner(), &self.owner(), &mint, &Token::id(), - )) - .instruction(Instruction { + ), + Instruction { program_id: mango_v4::id(), accounts: { let mut ams = anchor_lang::ToAccountMetas::to_account_metas( @@ -416,19 +385,21 @@ impl MangoClient { amount, allow_borrow, }), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error) + }, + ]; + self.send_and_confirm_owner_tx(ixs).await } - pub fn get_oracle_price( + pub async fn get_oracle_price( &self, token_name: &str, ) -> Result { let token_index = *self.context.token_indexes_by_name.get(token_name).unwrap(); let mint_info = self.context.mint_info(token_index); - let oracle_account = self.account_fetcher.fetch_raw_account(&mint_info.oracle)?; + let oracle_account = self + .account_fetcher + .fetch_raw_account(&mint_info.oracle) + .await?; Ok(pyth_sdk_solana::load_price(&oracle_account.data()).unwrap()) } @@ -436,7 +407,7 @@ impl MangoClient { // Serum3 // - pub fn serum3_create_open_orders(&self, name: &str) -> anyhow::Result { + pub async fn serum3_create_open_orders(&self, name: &str) -> anyhow::Result { let account_pubkey = self.mango_account_address; let market_index = *self @@ -452,37 +423,33 @@ impl MangoClient { b"Serum3OO".as_ref(), serum3_info.address.as_ref(), ], - &self.program().id(), + &mango_v4::ID, ) .0; - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::Serum3CreateOpenOrders { - group: self.group(), - account: account_pubkey, + let ix = Instruction { + program_id: mango_v4::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::Serum3CreateOpenOrders { + group: self.group(), + account: account_pubkey, - serum_market: serum3_info.address, - serum_program: serum3_info.market.serum_program, - serum_market_external: serum3_info.market.serum_market_external, - open_orders, - owner: self.owner(), - payer: self.owner(), - system_program: System::id(), - rent: sysvar::rent::id(), - }, - None, - ), - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::Serum3CreateOpenOrders {}, - ), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error) + serum_market: serum3_info.address, + serum_program: serum3_info.market.serum_program, + serum_market_external: serum3_info.market.serum_market_external, + open_orders, + owner: self.owner(), + payer: self.owner(), + system_program: System::id(), + rent: sysvar::rent::id(), + }, + None, + ), + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::Serum3CreateOpenOrders {}, + ), + }; + self.send_and_confirm_owner_tx(vec![ix]).await } fn serum3_data_by_market_name<'a>(&'a self, name: &str) -> Result, ClientError> { @@ -512,7 +479,7 @@ impl MangoClient { } #[allow(clippy::too_many_arguments)] - pub fn serum3_place_order( + pub async fn serum3_place_order( &self, name: &str, side: Serum3Side, @@ -525,10 +492,12 @@ impl MangoClient { ) -> anyhow::Result { let s3 = self.serum3_data_by_market_name(name)?; - let account = self.mango_account()?; + let account = self.mango_account().await?; let open_orders = account.serum3_orders(s3.market_index).unwrap().open_orders; - let health_check_metas = self.derive_health_check_remaining_account_metas(vec![], false)?; + let health_check_metas = self + .derive_health_check_remaining_account_metas(vec![], false) + .await?; // https://github.com/project-serum/serum-ts/blob/master/packages/serum/src/market.ts#L1306 let limit_price = { @@ -591,77 +560,24 @@ impl MangoClient { Serum3Side::Ask => s3.base.mint_info, }; - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::Serum3PlaceOrder { - group: self.group(), - account: self.mango_account_address, - open_orders, - payer_bank: payer_mint_info.first_bank(), - payer_vault: payer_mint_info.first_vault(), - payer_oracle: payer_mint_info.oracle, - serum_market: s3.market.address, - serum_program: s3.market.market.serum_program, - serum_market_external: s3.market.market.serum_market_external, - market_bids: s3.market.bids, - market_asks: s3.market.asks, - market_event_queue: s3.market.event_q, - market_request_queue: s3.market.req_q, - market_base_vault: s3.market.coin_vault, - market_quote_vault: s3.market.pc_vault, - market_vault_signer: s3.market.vault_signer, - owner: self.owner(), - token_program: Token::id(), - }, - None, - ); - ams.extend(health_check_metas.into_iter()); - ams - }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::Serum3PlaceOrder { - side, - limit_price, - max_base_qty, - max_native_quote_qty_including_fees, - self_trade_behavior, - order_type, - client_order_id, - limit, - }, - ), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error) - } - - pub fn serum3_settle_funds(&self, name: &str) -> anyhow::Result { - let s3 = self.serum3_data_by_market_name(name)?; - - let account = self.mango_account()?; - let open_orders = account.serum3_orders(s3.market_index).unwrap().open_orders; - - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::Serum3SettleFunds { + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::Serum3PlaceOrder { group: self.group(), account: self.mango_account_address, open_orders, - quote_bank: s3.quote.mint_info.first_bank(), - quote_vault: s3.quote.mint_info.first_vault(), - base_bank: s3.base.mint_info.first_bank(), - base_vault: s3.base.mint_info.first_vault(), + payer_bank: payer_mint_info.first_bank(), + payer_vault: payer_mint_info.first_vault(), + payer_oracle: payer_mint_info.oracle, serum_market: s3.market.address, serum_program: s3.market.market.serum_program, serum_market_external: s3.market.market.serum_market_external, + market_bids: s3.market.bids, + market_asks: s3.market.asks, + market_event_queue: s3.market.event_q, + market_request_queue: s3.market.req_q, market_base_vault: s3.market.coin_vault, market_quote_vault: s3.market.pc_vault, market_vault_signer: s3.market.vault_signer, @@ -669,25 +585,69 @@ impl MangoClient { token_program: Token::id(), }, None, - ), - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::Serum3SettleFunds {}, - ), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error) + ); + ams.extend(health_check_metas.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::Serum3PlaceOrder { + side, + limit_price, + max_base_qty, + max_native_quote_qty_including_fees, + self_trade_behavior, + order_type, + client_order_id, + limit, + }), + }; + self.send_and_confirm_owner_tx(vec![ix]).await } - pub fn serum3_cancel_all_orders(&self, market_name: &str) -> Result, anyhow::Error> { + pub async fn serum3_settle_funds(&self, name: &str) -> anyhow::Result { + let s3 = self.serum3_data_by_market_name(name)?; + + let account = self.mango_account().await?; + let open_orders = account.serum3_orders(s3.market_index).unwrap().open_orders; + + let ix = Instruction { + program_id: mango_v4::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::Serum3SettleFunds { + group: self.group(), + account: self.mango_account_address, + open_orders, + quote_bank: s3.quote.mint_info.first_bank(), + quote_vault: s3.quote.mint_info.first_vault(), + base_bank: s3.base.mint_info.first_bank(), + base_vault: s3.base.mint_info.first_vault(), + serum_market: s3.market.address, + serum_program: s3.market.market.serum_program, + serum_market_external: s3.market.market.serum_market_external, + market_base_vault: s3.market.coin_vault, + market_quote_vault: s3.market.pc_vault, + market_vault_signer: s3.market.vault_signer, + owner: self.owner(), + token_program: Token::id(), + }, + None, + ), + data: anchor_lang::InstructionData::data(&mango_v4::instruction::Serum3SettleFunds {}), + }; + self.send_and_confirm_owner_tx(vec![ix]).await + } + + pub async fn serum3_cancel_all_orders( + &self, + market_name: &str, + ) -> Result, anyhow::Error> { let market_index = *self .context .serum3_market_indexes_by_name .get(market_name) .unwrap(); - let account = self.mango_account()?; + let account = self.mango_account().await?; let open_orders = account.serum3_orders(market_index).unwrap().open_orders; - let open_orders_acc = self.account_fetcher.fetch_raw_account(&open_orders)?; + let open_orders_acc = self.account_fetcher.fetch_raw_account(&open_orders).await?; let open_orders_bytes = open_orders_acc.data(); let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes( &open_orders_bytes[5..5 + std::mem::size_of::()], @@ -698,8 +658,10 @@ impl MangoClient { if order_id != 0 { // TODO: find side for order_id, and only cancel the relevant order self.serum3_cancel_order(market_name, Serum3Side::Bid, order_id) + .await .ok(); self.serum3_cancel_order(market_name, Serum3Side::Ask, order_id) + .await .ok(); orders.push(order_id); @@ -709,7 +671,7 @@ impl MangoClient { Ok(orders) } - pub fn serum3_liq_force_cancel_orders( + pub async fn serum3_liq_force_cancel_orders( &self, liqee: (&Pubkey, &MangoAccountValue), market_index: Serum3MarketIndex, @@ -722,91 +684,83 @@ impl MangoClient { .derive_health_check_remaining_account_metas(liqee.1, vec![], false) .unwrap(); - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::Serum3LiqForceCancelOrders { - group: self.group(), - account: *liqee.0, - open_orders: *open_orders, - serum_market: s3.market.address, - serum_program: s3.market.market.serum_program, - serum_market_external: s3.market.market.serum_market_external, - market_bids: s3.market.bids, - market_asks: s3.market.asks, - market_event_queue: s3.market.event_q, - market_base_vault: s3.market.coin_vault, - market_quote_vault: s3.market.pc_vault, - market_vault_signer: s3.market.vault_signer, - quote_bank: s3.quote.mint_info.first_bank(), - quote_vault: s3.quote.mint_info.first_vault(), - base_bank: s3.base.mint_info.first_bank(), - base_vault: s3.base.mint_info.first_vault(), - token_program: Token::id(), - }, - None, - ); - ams.extend(health_remaining_ams.into_iter()); - ams - }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::Serum3LiqForceCancelOrders { limit: 5 }, - ), - }) - .send() - .map_err(prettify_client_error) + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::Serum3LiqForceCancelOrders { + group: self.group(), + account: *liqee.0, + open_orders: *open_orders, + serum_market: s3.market.address, + serum_program: s3.market.market.serum_program, + serum_market_external: s3.market.market.serum_market_external, + market_bids: s3.market.bids, + market_asks: s3.market.asks, + market_event_queue: s3.market.event_q, + market_base_vault: s3.market.coin_vault, + market_quote_vault: s3.market.pc_vault, + market_vault_signer: s3.market.vault_signer, + quote_bank: s3.quote.mint_info.first_bank(), + quote_vault: s3.quote.mint_info.first_vault(), + base_bank: s3.base.mint_info.first_bank(), + base_vault: s3.base.mint_info.first_vault(), + token_program: Token::id(), + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::Serum3LiqForceCancelOrders { limit: 5 }, + ), + }; + self.send_and_confirm_permissionless_tx(vec![ix]).await } - pub fn serum3_cancel_order( + pub async fn serum3_cancel_order( &self, market_name: &str, side: Serum3Side, order_id: u128, - ) -> anyhow::Result<()> { + ) -> anyhow::Result { let s3 = self.serum3_data_by_market_name(market_name)?; - let account = self.mango_account()?; + let account = self.mango_account().await?; let open_orders = account.serum3_orders(s3.market_index).unwrap().open_orders; - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::Serum3CancelOrder { - group: self.group(), - account: self.mango_account_address, - serum_market: s3.market.address, - serum_program: s3.market.market.serum_program, - serum_market_external: s3.market.market.serum_market_external, - open_orders, - market_bids: s3.market.bids, - market_asks: s3.market.asks, - market_event_queue: s3.market.event_q, - owner: self.owner(), - }, - None, - ) - }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::Serum3CancelOrder { side, order_id }, - ), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error)?; - - Ok(()) + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::Serum3CancelOrder { + group: self.group(), + account: self.mango_account_address, + serum_market: s3.market.address, + serum_program: s3.market.market.serum_program, + serum_market_external: s3.market.market.serum_market_external, + open_orders, + market_bids: s3.market.bids, + market_asks: s3.market.asks, + market_event_queue: s3.market.event_q, + owner: self.owner(), + }, + None, + ) + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::Serum3CancelOrder { + side, + order_id, + }), + }; + self.send_and_confirm_owner_tx(vec![ix]).await } // // Perps // - pub fn perp_settle_pnl( + pub async fn perp_settle_pnl( &self, market_index: PerpMarketIndex, account_a: (&Pubkey, &MangoAccountValue), @@ -820,36 +774,32 @@ impl MangoClient { .derive_health_check_remaining_account_metas_two_accounts(account_a.1, account_b.1, &[]) .unwrap(); - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpSettlePnl { - group: self.group(), - settler: self.mango_account_address, - settler_owner: self.owner(), - perp_market: perp.address, - account_a: *account_a.0, - account_b: *account_b.0, - oracle: perp.market.oracle, - settle_bank: settlement_token.mint_info.first_bank(), - settle_oracle: settlement_token.mint_info.oracle, - }, - None, - ); - ams.extend(health_remaining_ams.into_iter()); - ams - }, - data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpSettlePnl {}), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error) + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpSettlePnl { + group: self.group(), + settler: self.mango_account_address, + settler_owner: self.owner(), + perp_market: perp.address, + account_a: *account_a.0, + account_b: *account_b.0, + oracle: perp.market.oracle, + settle_bank: settlement_token.mint_info.first_bank(), + settle_oracle: settlement_token.mint_info.oracle, + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpSettlePnl {}), + }; + self.send_and_confirm_permissionless_tx(vec![ix]).await } - pub fn perp_liq_force_cancel_orders( + pub async fn perp_liq_force_cancel_orders( &self, liqee: (&Pubkey, &MangoAccountValue), market_index: PerpMarketIndex, @@ -861,33 +811,30 @@ impl MangoClient { .derive_health_check_remaining_account_metas(liqee.1, vec![], false) .unwrap(); - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpLiqForceCancelOrders { - group: self.group(), - account: *liqee.0, - perp_market: perp.address, - bids: perp.market.bids, - asks: perp.market.asks, - }, - None, - ); - ams.extend(health_remaining_ams.into_iter()); - ams - }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::PerpLiqForceCancelOrders { limit: 5 }, - ), - }) - .send() - .map_err(prettify_client_error) + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpLiqForceCancelOrders { + group: self.group(), + account: *liqee.0, + perp_market: perp.address, + bids: perp.market.bids, + asks: perp.market.asks, + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::PerpLiqForceCancelOrders { limit: 5 }, + ), + }; + self.send_and_confirm_permissionless_tx(vec![ix]).await } - pub fn perp_liq_base_position( + pub async fn perp_liq_base_position( &self, liqee: (&Pubkey, &MangoAccountValue), market_index: PerpMarketIndex, @@ -897,37 +844,34 @@ impl MangoClient { let health_remaining_ams = self .derive_liquidation_health_check_remaining_account_metas(liqee.1, &[]) + .await .unwrap(); - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpLiqBasePosition { - group: self.group(), - perp_market: perp.address, - oracle: perp.market.oracle, - liqor: self.mango_account_address, - liqor_owner: self.owner(), - liqee: *liqee.0, - }, - None, - ); - ams.extend(health_remaining_ams.into_iter()); - ams - }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::PerpLiqBasePosition { max_base_transfer }, - ), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error) + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpLiqBasePosition { + group: self.group(), + perp_market: perp.address, + oracle: perp.market.oracle, + liqor: self.mango_account_address, + liqor_owner: self.owner(), + liqee: *liqee.0, + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpLiqBasePosition { + max_base_transfer, + }), + }; + self.send_and_confirm_owner_tx(vec![ix]).await } - pub fn perp_liq_bankruptcy( + pub async fn perp_liq_bankruptcy( &self, liqee: (&Pubkey, &MangoAccountValue), market_index: PerpMarketIndex, @@ -936,52 +880,50 @@ impl MangoClient { let group = account_fetcher_fetch_anchor_account::( &*self.account_fetcher, &self.context.group, - )?; + ) + .await?; let perp = self.context.perp(market_index); let settle_token_info = self.context.token(perp.market.settle_token_index); let health_remaining_ams = self .derive_liquidation_health_check_remaining_account_metas(liqee.1, &[]) + .await .unwrap(); - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpLiqBankruptcy { - group: self.group(), - perp_market: perp.address, - liqor: self.mango_account_address, - liqor_owner: self.owner(), - liqee: *liqee.0, - settle_bank: settle_token_info.mint_info.first_bank(), - settle_vault: settle_token_info.mint_info.first_vault(), - settle_oracle: settle_token_info.mint_info.oracle, - insurance_vault: group.insurance_vault, - token_program: Token::id(), - }, - None, - ); - ams.extend(health_remaining_ams.into_iter()); - ams - }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::PerpLiqBankruptcy { max_liab_transfer }, - ), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error) + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpLiqBankruptcy { + group: self.group(), + perp_market: perp.address, + liqor: self.mango_account_address, + liqor_owner: self.owner(), + liqee: *liqee.0, + settle_bank: settle_token_info.mint_info.first_bank(), + settle_vault: settle_token_info.mint_info.first_vault(), + settle_oracle: settle_token_info.mint_info.oracle, + insurance_vault: group.insurance_vault, + token_program: Token::id(), + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpLiqBankruptcy { + max_liab_transfer, + }), + }; + self.send_and_confirm_owner_tx(vec![ix]).await } // // Liquidation // - pub fn token_liq_with_token( + pub async fn token_liq_with_token( &self, liqee: (&Pubkey, &MangoAccountValue), asset_token_index: TokenIndex, @@ -993,39 +935,34 @@ impl MangoClient { liqee.1, &[asset_token_index, liab_token_index], ) + .await .unwrap(); - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::TokenLiqWithToken { - group: self.group(), - liqee: *liqee.0, - liqor: self.mango_account_address, - liqor_owner: self.owner(), - }, - None, - ); - ams.extend(health_remaining_ams); - ams - }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::TokenLiqWithToken { - asset_token_index, - liab_token_index, - max_liab_transfer, + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::TokenLiqWithToken { + group: self.group(), + liqee: *liqee.0, + liqor: self.mango_account_address, + liqor_owner: self.owner(), }, - ), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error) + None, + ); + ams.extend(health_remaining_ams); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenLiqWithToken { + asset_token_index, + liab_token_index, + max_liab_transfer, + }), + }; + self.send_and_confirm_owner_tx(vec![ix]).await } - pub fn token_liq_bankruptcy( + pub async fn token_liq_bankruptcy( &self, liqee: (&Pubkey, &MangoAccountValue), liab_token_index: TokenIndex, @@ -1048,67 +985,43 @@ impl MangoClient { liqee.1, &[quote_token_index, liab_token_index], ) + .await .unwrap(); let group = account_fetcher_fetch_anchor_account::( &*self.account_fetcher, &self.context.group, - )?; + ) + .await?; - self.program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::TokenLiqBankruptcy { - group: self.group(), - liqee: *liqee.0, - liqor: self.mango_account_address, - liqor_owner: self.owner(), - liab_mint_info: liab_info.mint_info_address, - quote_vault: quote_info.mint_info.first_vault(), - insurance_vault: group.insurance_vault, - token_program: Token::id(), - }, - None, - ); - ams.extend(bank_remaining_ams); - ams.extend(health_remaining_ams); - ams - }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::TokenLiqBankruptcy { max_liab_transfer }, - ), - }) - .signer(&self.owner) - .send() - .map_err(prettify_client_error) + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::TokenLiqBankruptcy { + group: self.group(), + liqee: *liqee.0, + liqor: self.mango_account_address, + liqor_owner: self.owner(), + liab_mint_info: liab_info.mint_info_address, + quote_vault: quote_info.mint_info.first_vault(), + insurance_vault: group.insurance_vault, + token_program: Token::id(), + }, + None, + ); + ams.extend(bank_remaining_ams); + ams.extend(health_remaining_ams); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenLiqBankruptcy { + max_liab_transfer, + }), + }; + self.send_and_confirm_owner_tx(vec![ix]).await } - pub fn jupiter_swap( - &self, - input_mint: Pubkey, - output_mint: Pubkey, - amount: u64, - slippage: f64, - swap_mode: JupiterSwapMode, - ) -> anyhow::Result { - self.invoke(self.jupiter_swap_async(input_mint, output_mint, amount, slippage, swap_mode)) - } - - pub fn jupiter_route( - &self, - input_mint: Pubkey, - output_mint: Pubkey, - amount: u64, - slippage: f64, - swap_mode: JupiterSwapMode, - ) -> anyhow::Result { - self.invoke(self.jupiter_route_async(input_mint, output_mint, amount, slippage, swap_mode)) - } - - pub async fn jupiter_route_async( + pub async fn jupiter_route( &self, input_mint: Pubkey, output_mint: Pubkey, @@ -1118,14 +1031,15 @@ impl MangoClient { ) -> anyhow::Result { let quote = self .http_client - .get("https://quote-api.jup.ag/v1/quote") + .get("https://quote-api.jup.ag/v4/quote") .query(&[ ("inputMint", input_mint.to_string()), ("outputMint", output_mint.to_string()), ("amount", format!("{}", amount)), ("onlyDirectRoutes", "true".into()), + ("enforceSingleTx", "true".into()), ("filterTopNResult", "10".into()), - ("slippage", format!("{}", slippage)), + ("slippageBps", format!("{}", slippage)), ( "swapMode", match swap_mode { @@ -1162,7 +1076,7 @@ impl MangoClient { Ok(route.clone()) } - pub async fn jupiter_swap_async( + pub async fn jupiter_swap( &self, input_mint: Pubkey, output_mint: Pubkey, @@ -1173,12 +1087,12 @@ impl MangoClient { let source_token = self.context.token_by_mint(&input_mint)?; let target_token = self.context.token_by_mint(&output_mint)?; let route = self - .jupiter_route_async(input_mint, output_mint, amount, slippage, swap_mode) + .jupiter_route(input_mint, output_mint, amount, slippage, swap_mode) .await?; let swap = self .http_client - .post("https://quote-api.jup.ag/v1/swap") + .post("https://quote-api.jup.ag/v4/swap") .json(&jupiter::SwapRequest { route: route.clone(), user_public_key: self.owner.pubkey().to_string(), @@ -1197,11 +1111,10 @@ impl MangoClient { ); } - // TODO: deal with versioned transaction! let jup_tx = bincode::options() .with_fixint_encoding() .reject_trailing_bytes() - .deserialize::( + .deserialize::( &base64::decode(&swap.swap_transaction) .context("base64 decoding jupiter transaction")?, ) @@ -1209,7 +1122,10 @@ impl MangoClient { let ata_program = anchor_spl::associated_token::ID; let token_program = anchor_spl::token::ID; let is_setup_ix = |k: Pubkey| -> bool { k == ata_program || k == token_program }; - let jup_ixs = deserialize_instructions(&jup_tx.message) + let (jup_ixs, jup_alts) = self + .deserialize_instructions_and_alts(&jup_tx.message) + .await?; + let filtered_jup_ix = jup_ixs .into_iter() .filter(|ix| !is_setup_ix(ix.program_id)) .collect::>(); @@ -1246,7 +1162,7 @@ impl MangoClient { match swap_mode { JupiterSwapMode::ExactIn => amount, // in amount + slippage - JupiterSwapMode::ExactOut => route.other_amount_threshold, + JupiterSwapMode::ExactOut => u64::from_str(&route.other_amount_threshold).unwrap(), }, 0u64, ]; @@ -1257,12 +1173,12 @@ impl MangoClient { vec![source_token.token_index, target_token.token_index], true, ) + .await .context("building health accounts")?; - let program = self.program(); - let mut builder = program.request(); + let mut instructions = Vec::new(); - builder = builder.instruction( + instructions.push( spl_associated_token_account::instruction::create_associated_token_account_idempotent( &self.owner.pubkey(), &self.owner.pubkey(), @@ -1270,7 +1186,7 @@ impl MangoClient { &Token::id(), ), ); - builder = builder.instruction( + instructions.push( spl_associated_token_account::instruction::create_associated_token_account_idempotent( &self.owner.pubkey(), &self.owner.pubkey(), @@ -1278,7 +1194,7 @@ impl MangoClient { &Token::id(), ), ); - builder = builder.instruction(Instruction { + instructions.push(Instruction { program_id: mango_v4::id(), accounts: { let mut ams = anchor_lang::ToAccountMetas::to_account_metas( @@ -1300,10 +1216,10 @@ impl MangoClient { loan_amounts, }), }); - for ix in jup_ixs { - builder = builder.instruction(ix.clone()); + for ix in filtered_jup_ix { + instructions.push(ix.clone()); } - builder = builder.instruction(Instruction { + instructions.push(Instruction { program_id: mango_v4::id(), accounts: { let mut ams = anchor_lang::ToAccountMetas::to_account_metas( @@ -1326,39 +1242,118 @@ impl MangoClient { }); let rpc = self.client.rpc_async(); - builder - .signer(&self.owner) - .send_rpc_async(&rpc) - .await - .map_err(prettify_client_error) + let payer = self.owner.pubkey(); // maybe use fee_payer? but usually it's the same + let mut address_lookup_tables = self.mango_address_lookup_tables().await?; + address_lookup_tables.extend(jup_alts.into_iter()); + + TransactionBuilder { + instructions, + address_lookup_tables, + payer, + signers: vec![&self.owner], + } + .send_and_confirm(&rpc) + .await } - fn invoke>(&self, f: F) -> T { - // `block_on()` panics if called within an asynchronous execution context. Whereas - // `block_in_place()` only panics if called from a current_thread runtime, which is the - // lesser evil. - tokio::task::block_in_place(move || self.runtime.as_ref().expect("runtime").block_on(f)) - } -} - -fn deserialize_instructions(message: &solana_sdk::message::Message) -> Vec { - message - .instructions - .iter() - .map(|ci| solana_sdk::instruction::Instruction { - program_id: *ci.program_id(&message.account_keys), - accounts: ci - .accounts - .iter() - .map(|&index| AccountMeta { - pubkey: message.account_keys[index as usize], - is_signer: message.is_signer(index.into()), - is_writable: message.is_writable(index.into()), - }) - .collect(), - data: ci.data.clone(), + async fn fetch_address_lookup_table( + &self, + address: Pubkey, + ) -> anyhow::Result { + let raw = self + .account_fetcher + .fetch_raw_account_lookup_table(&address) + .await?; + let data = AddressLookupTable::deserialize(&raw.data())?; + Ok(AddressLookupTableAccount { + key: address, + addresses: data.addresses.to_vec(), }) - .collect() + } + + async fn mango_address_lookup_tables(&self) -> anyhow::Result> { + stream::iter(self.context.address_lookup_tables.iter()) + .then(|&k| self.fetch_address_lookup_table(k)) + .try_collect::>() + .await + } + + async fn deserialize_instructions_and_alts( + &self, + message: &solana_sdk::message::VersionedMessage, + ) -> anyhow::Result<(Vec, Vec)> { + let lookups = message.address_table_lookups().unwrap_or_default(); + let address_lookup_tables = stream::iter(lookups) + .then(|a| self.fetch_address_lookup_table(a.account_key)) + .try_collect::>() + .await?; + + let mut account_keys = message.static_account_keys().to_vec(); + for (lookups, table) in lookups.iter().zip(address_lookup_tables.iter()) { + account_keys.extend( + lookups + .writable_indexes + .iter() + .map(|&index| table.addresses[index as usize]), + ); + } + for (lookups, table) in lookups.iter().zip(address_lookup_tables.iter()) { + account_keys.extend( + lookups + .readonly_indexes + .iter() + .map(|&index| table.addresses[index as usize]), + ); + } + + let compiled_ix = message + .instructions() + .iter() + .map(|ci| solana_sdk::instruction::Instruction { + program_id: *ci.program_id(&account_keys), + accounts: ci + .accounts + .iter() + .map(|&index| AccountMeta { + pubkey: account_keys[index as usize], + is_signer: message.is_signer(index.into()), + is_writable: message.is_maybe_writable(index.into()), + }) + .collect(), + data: ci.data.clone(), + }) + .collect(); + + Ok((compiled_ix, address_lookup_tables)) + } + + pub async fn send_and_confirm_owner_tx( + &self, + instructions: Vec, + ) -> anyhow::Result { + TransactionBuilder { + instructions, + address_lookup_tables: vec![], + payer: self.client.fee_payer.pubkey(), + signers: vec![&self.owner, &*self.client.fee_payer], + } + .send_and_confirm(&self.client.rpc_async()) + .await + } + + pub async fn send_and_confirm_permissionless_tx( + &self, + instructions: Vec, + ) -> anyhow::Result { + TransactionBuilder { + instructions, + address_lookup_tables: vec![], + payer: self.client.fee_payer.pubkey(), + signers: vec![&*self.client.fee_payer], + } + .send_and_confirm(&self.client.rpc_async()) + .await + } } struct Serum3Data<'a> { @@ -1374,31 +1369,80 @@ pub enum MangoClientError { SendTransactionPreflightFailure { logs: String }, } +pub struct TransactionBuilder<'a> { + instructions: Vec, + address_lookup_tables: Vec, + signers: Vec<&'a dyn Signer>, + payer: Pubkey, +} + +impl<'a> TransactionBuilder<'a> { + pub async fn transaction( + self, + rpc: &RpcClientAsync, + ) -> anyhow::Result { + let latest_blockhash = rpc.get_latest_blockhash().await?; + self.transaction_with_blockhash(latest_blockhash) + } + + pub fn transaction_with_blockhash( + self, + blockhash: Hash, + ) -> anyhow::Result { + let v0_message = solana_sdk::message::v0::Message::try_compile( + &self.payer, + &self.instructions, + &self.address_lookup_tables, + blockhash, + )?; + let versioned_message = solana_sdk::message::VersionedMessage::V0(v0_message); + let signers = self + .signers + .into_iter() + .unique_by(|s| s.pubkey()) + .collect::>(); + let tx = + solana_sdk::transaction::VersionedTransaction::try_new(versioned_message, &signers)?; + Ok(tx) + } + + pub async fn send_and_confirm(self, rpc: &RpcClientAsync) -> anyhow::Result { + let tx = self.transaction(rpc).await?; + rpc.send_and_confirm_transaction(&tx) + .await + .map_err(prettify_solana_client_error) + } +} + /// Do some manual unpacking on some ClientErrors /// /// Unfortunately solana's RpcResponseError will very unhelpfully print [N log messages] /// instead of showing the actual log messages. This unpacks the error to provide more useful /// output. pub fn prettify_client_error(err: anchor_client::ClientError) -> anyhow::Error { + match err { + anchor_client::ClientError::SolanaClientError(c) => prettify_solana_client_error(c), + _ => err.into(), + } +} + +pub fn prettify_solana_client_error( + err: solana_client::client_error::ClientError, +) -> anyhow::Error { use solana_client::client_error::ClientErrorKind; use solana_client::rpc_request::{RpcError, RpcResponseErrorData}; - match &err { - anchor_client::ClientError::SolanaClientError(c) => { - match c.kind() { - ClientErrorKind::RpcError(RpcError::RpcResponseError { data, .. }) => match data { - RpcResponseErrorData::SendTransactionPreflightFailure(s) => { - if let Some(logs) = s.logs.as_ref() { - return MangoClientError::SendTransactionPreflightFailure { - logs: logs.iter().join("; "), - } - .into(); - } + match err.kind() { + ClientErrorKind::RpcError(RpcError::RpcResponseError { data, .. }) => match data { + RpcResponseErrorData::SendTransactionPreflightFailure(s) => { + if let Some(logs) = s.logs.as_ref() { + return MangoClientError::SendTransactionPreflightFailure { + logs: logs.iter().join("; "), } - _ => {} - }, - _ => {} - }; - } + .into(); + } + } + _ => {} + }, _ => {} }; err.into() diff --git a/client/src/context.rs b/client/src/context.rs index 8e9bd4094..ae46b0217 100644 --- a/client/src/context.rs +++ b/client/src/context.rs @@ -1,23 +1,24 @@ use std::collections::HashMap; -use anchor_client::{Client, ClientError, Cluster, Program}; +use anchor_client::ClientError; use anchor_lang::__private::bytemuck; use mango_v4::state::{ - MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market, Serum3MarketIndex, - TokenIndex, + Group, MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market, + Serum3MarketIndex, TokenIndex, }; use fixed::types::I80F48; +use futures::{stream, StreamExt, TryStreamExt}; use itertools::Itertools; use crate::gpa::*; +use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_sdk::account::Account; use solana_sdk::instruction::AccountMeta; -use solana_sdk::signature::Keypair; -use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey}; +use solana_sdk::pubkey::Pubkey; #[derive(Clone)] pub struct TokenContext { @@ -64,6 +65,8 @@ pub struct MangoGroupContext { pub perp_markets: HashMap, pub perp_market_indexes_by_name: HashMap, + + pub address_lookup_tables: Vec, } impl MangoGroupContext { @@ -94,17 +97,11 @@ impl MangoGroupContext { self.perp(perp_market_index).address } - pub fn new_from_rpc( - group: Pubkey, - cluster: Cluster, - commitment: CommitmentConfig, - ) -> Result { - let program = - Client::new_with_options(cluster, std::rc::Rc::new(Keypair::new()), commitment) - .program(mango_v4::ID); + pub async fn new_from_rpc(rpc: &RpcClientAsync, group: Pubkey) -> anyhow::Result { + let program = mango_v4::ID; // tokens - let mint_info_tuples = fetch_mint_infos(&program, group)?; + let mint_info_tuples = fetch_mint_infos(rpc, program, group).await?; let mut tokens = mint_info_tuples .iter() .map(|(pk, mi)| { @@ -124,7 +121,7 @@ impl MangoGroupContext { // reading the banks is only needed for the token names and decimals // FUTURE: either store the names on MintInfo as well, or maybe don't store them at all // because they are in metaplex? - let bank_tuples = fetch_banks(&program, group)?; + let bank_tuples = fetch_banks(rpc, program, group).await?; for (_, bank) in bank_tuples { let token = tokens.get_mut(&bank.token_index).unwrap(); token.name = bank.name().into(); @@ -133,11 +130,15 @@ impl MangoGroupContext { assert!(tokens.values().all(|t| t.decimals != u8::MAX)); // serum3 markets - let serum3_market_tuples = fetch_serum3_markets(&program, group)?; + let serum3_market_tuples = fetch_serum3_markets(rpc, program, group).await?; + let serum3_markets_external = stream::iter(serum3_market_tuples.iter()) + .then(|(_, s)| fetch_raw_account(rpc, s.serum_market_external)) + .try_collect::>() + .await?; let serum3_markets = serum3_market_tuples .iter() - .map(|(pk, s)| { - let market_external_account = fetch_raw_account(&program, s.serum_market_external)?; + .zip(serum3_markets_external.iter()) + .map(|((pk, s), market_external_account)| { let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes( &market_external_account.data [5..5 + std::mem::size_of::()], @@ -148,7 +149,7 @@ impl MangoGroupContext { &s.serum_program, ) .unwrap(); - Ok(( + ( s.market_index, Serum3MarketContext { address: *pk, @@ -163,12 +164,12 @@ impl MangoGroupContext { coin_lot_size: market_external.coin_lot_size, pc_lot_size: market_external.pc_lot_size, }, - )) + ) }) - .collect::, ClientError>>()?; + .collect::>(); // perp markets - let perp_market_tuples = fetch_perp_markets(&program, group)?; + let perp_market_tuples = fetch_perp_markets(rpc, program, group).await?; let perp_markets = perp_market_tuples .iter() .map(|(pk, pm)| { @@ -196,6 +197,14 @@ impl MangoGroupContext { .map(|(i, p)| (p.market.name().to_string(), *i)) .collect::>(); + let group_data = fetch_anchor_account::(rpc, &group).await?; + let address_lookup_tables = group_data + .address_lookup_tables + .iter() + .filter(|&&k| k != Pubkey::default()) + .cloned() + .collect::>(); + Ok(MangoGroupContext { group, tokens, @@ -204,6 +213,7 @@ impl MangoGroupContext { serum3_market_indexes_by_name, perp_markets, perp_market_indexes_by_name, + address_lookup_tables, }) } @@ -331,9 +341,9 @@ fn from_serum_style_pubkey(d: [u64; 4]) -> Pubkey { Pubkey::new(bytemuck::cast_slice(&d as &[_])) } -fn fetch_raw_account(program: &Program, address: Pubkey) -> Result { - let rpc = program.rpc(); - rpc.get_account_with_commitment(&address, rpc.commitment())? +async fn fetch_raw_account(rpc: &RpcClientAsync, address: Pubkey) -> Result { + rpc.get_account_with_commitment(&address, rpc.commitment()) + .await? .value .ok_or(ClientError::AccountNotFound) } diff --git a/client/src/gpa.rs b/client/src/gpa.rs index 5e836ac24..db5fece0b 100644 --- a/client/src/gpa.rs +++ b/client/src/gpa.rs @@ -1,18 +1,19 @@ -use anchor_client::{ClientError, Program}; -use anchor_lang::Discriminator; +use anchor_lang::{AccountDeserialize, Discriminator}; use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market}; use solana_account_decoder::UiAccountEncoding; +use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}; use solana_client::rpc_filter::{Memcmp, RpcFilterType}; use solana_sdk::pubkey::Pubkey; -pub fn fetch_mango_accounts( - program: &Program, +pub async fn fetch_mango_accounts( + rpc: &RpcClientAsync, + program: Pubkey, group: Pubkey, owner: Pubkey, -) -> Result, ClientError> { +) -> anyhow::Result> { let config = RpcProgramAccountsConfig { filters: Some(vec![ RpcFilterType::Memcmp(Memcmp::new_base58_encoded( @@ -28,47 +29,103 @@ pub fn fetch_mango_accounts( }, ..RpcProgramAccountsConfig::default() }; - program - .rpc() - .get_program_accounts_with_config(&program.id(), config)? + rpc.get_program_accounts_with_config(&program, config) + .await? .into_iter() .map(|(key, account)| Ok((key, MangoAccountValue::from_bytes(&account.data[8..])?))) .collect::, _>>() } -pub fn fetch_banks(program: &Program, group: Pubkey) -> Result, ClientError> { - program.accounts::(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded( - 8, - &group.to_bytes(), - ))]) +pub async fn fetch_anchor_account( + rpc: &RpcClientAsync, + address: &Pubkey, +) -> anyhow::Result { + let account = rpc.get_account(address).await?; + Ok(T::try_deserialize(&mut (&account.data as &[u8]))?) } -pub fn fetch_mint_infos( - program: &Program, - group: Pubkey, -) -> Result, ClientError> { - program.accounts::(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded( - 8, - &group.to_bytes(), - ))]) +async fn fetch_anchor_accounts( + rpc: &RpcClientAsync, + program: Pubkey, + filters: Vec, +) -> anyhow::Result> { + let account_type_filter = + RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, &T::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, T::try_deserialize(&mut (&account.data as &[u8]))?))) + .collect() } -pub fn fetch_serum3_markets( - program: &Program, +pub async fn fetch_banks( + rpc: &RpcClientAsync, + program: Pubkey, group: Pubkey, -) -> Result, ClientError> { - program.accounts::(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded( - 8, - &group.to_bytes(), - ))]) +) -> anyhow::Result> { + fetch_anchor_accounts::( + rpc, + program, + vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded( + 8, + &group.to_bytes(), + ))], + ) + .await } -pub fn fetch_perp_markets( - program: &Program, +pub async fn fetch_mint_infos( + rpc: &RpcClientAsync, + program: Pubkey, group: Pubkey, -) -> Result, ClientError> { - program.accounts::(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded( - 8, - &group.to_bytes(), - ))]) +) -> anyhow::Result> { + fetch_anchor_accounts::( + rpc, + program, + vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded( + 8, + &group.to_bytes(), + ))], + ) + .await +} + +pub async fn fetch_serum3_markets( + rpc: &RpcClientAsync, + program: Pubkey, + group: Pubkey, +) -> anyhow::Result> { + fetch_anchor_accounts::( + rpc, + program, + vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded( + 8, + &group.to_bytes(), + ))], + ) + .await +} + +pub async fn fetch_perp_markets( + rpc: &RpcClientAsync, + program: Pubkey, + group: Pubkey, +) -> anyhow::Result> { + fetch_anchor_accounts::( + rpc, + program, + vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded( + 8, + &group.to_bytes(), + ))], + ) + .await } diff --git a/client/src/health_cache.rs b/client/src/health_cache.rs index bcf9faa02..f24177f8d 100644 --- a/client/src/health_cache.rs +++ b/client/src/health_cache.rs @@ -1,10 +1,11 @@ use crate::{AccountFetcher, MangoGroupContext}; use anyhow::Context; +use futures::{stream, StreamExt, TryStreamExt}; use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::health::{FixedOrderAccountRetriever, HealthCache}; use mango_v4::state::MangoAccountValue; -pub fn new( +pub async fn new( context: &MangoGroupContext, account_fetcher: &impl AccountFetcher, account: &MangoAccountValue, @@ -13,18 +14,18 @@ pub fn new( let active_perp_len = account.active_perp_positions().count(); let metas = context.derive_health_check_remaining_account_metas(account, vec![], false)?; - let accounts = metas - .iter() - .map(|meta| { + let accounts: anyhow::Result> = stream::iter(metas.iter()) + .then(|meta| async { Ok(KeyedAccountSharedData::new( meta.pubkey, - account_fetcher.fetch_raw_account(&meta.pubkey)?, + account_fetcher.fetch_raw_account(&meta.pubkey).await?, )) }) - .collect::>>()?; + .try_collect() + .await; let retriever = FixedOrderAccountRetriever { - ais: accounts, + ais: accounts?, n_banks: active_token_len, n_perps: active_perp_len, begin_perp: active_token_len * 2, diff --git a/client/src/jupiter.rs b/client/src/jupiter.rs index 56c2e9633..a9e5b40c5 100644 --- a/client/src/jupiter.rs +++ b/client/src/jupiter.rs @@ -5,20 +5,21 @@ use serde::{Deserialize, Serialize}; pub struct QueryResult { pub data: Vec, pub time_taken: f64, - pub context_slot: String, + pub context_slot: u64, } #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct QueryRoute { - pub in_amount: u64, - pub out_amount: u64, - pub amount: u64, - pub other_amount_threshold: u64, - pub out_amount_with_slippage: Option, - pub swap_mode: String, + pub in_amount: String, + pub out_amount: String, pub price_impact_pct: f64, pub market_infos: Vec, + pub amount: String, + pub slippage_bps: u64, + pub other_amount_threshold: String, + pub swap_mode: String, + pub fees: Option, } #[derive(Deserialize, Serialize, Debug, Clone)] @@ -28,22 +29,35 @@ pub struct QueryMarketInfo { pub label: String, pub input_mint: String, pub output_mint: String, - pub in_amount: u64, - pub out_amount: u64, + pub not_enough_liquidity: bool, + pub in_amount: String, + pub out_amount: String, + pub min_in_amount: Option, + pub min_out_amount: Option, + pub price_impact_pct: Option, pub lp_fee: QueryFee, pub platform_fee: QueryFee, - pub not_enough_liquidity: bool, - pub price_impact_pct: Option, } #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct QueryFee { - pub amount: u64, + pub amount: String, pub mint: String, pub pct: Option, } +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct QueryRouteFees { + pub signature_fee: f64, + pub open_orders_deposits: Vec, + pub ata_deposits: Vec, + pub total_fee_and_deposits: f64, + #[serde(rename = "minimalSOLForTransaction")] + pub minimal_sol_for_transaction: f64, +} + #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct SwapRequest { diff --git a/client/src/perp_pnl.rs b/client/src/perp_pnl.rs index a0406a85a..d0a1fefc1 100644 --- a/client/src/perp_pnl.rs +++ b/client/src/perp_pnl.rs @@ -15,7 +15,7 @@ pub enum Direction { /// Returns up to `count` accounts with highest abs pnl (by `direction`) in descending order. /// Note: keep in sync with perp.ts:getSettlePnlCandidates -pub fn fetch_top( +pub async fn fetch_top( context: &crate::context::MangoGroupContext, account_fetcher: &impl AccountFetcher, perp_market_index: PerpMarketIndex, @@ -25,20 +25,23 @@ pub fn fetch_top( use std::time::{SystemTime, UNIX_EPOCH}; let now_ts: u64 = SystemTime::now() .duration_since(UNIX_EPOCH)? - .as_millis() + .as_secs() .try_into()?; let perp = context.perp(perp_market_index); let perp_market = - account_fetcher_fetch_anchor_account::(account_fetcher, &perp.address)?; - let oracle_acc = account_fetcher.fetch_raw_account(&perp_market.oracle)?; + account_fetcher_fetch_anchor_account::(account_fetcher, &perp.address).await?; + let oracle_acc = account_fetcher + .fetch_raw_account(&perp_market.oracle) + .await?; let oracle_price = perp_market.oracle_price( &KeyedAccountSharedData::new(perp_market.oracle, oracle_acc), None, )?; - let accounts = - account_fetcher.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())?; + let accounts = account_fetcher + .fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator()) + .await?; let mut accounts_pnl = accounts .iter() @@ -54,6 +57,7 @@ pub fn fetch_top( return None; } let mut perp_pos = perp_pos.unwrap().clone(); + perp_pos.settle_funding(&perp_market); perp_pos.update_settle_limit(&perp_market, now_ts); let pnl = perp_pos.pnl_for_price(&perp_market, oracle_price).unwrap(); let limited_pnl = perp_pos.apply_pnl_settle_limit(pnl, &perp_market); @@ -88,8 +92,9 @@ pub fn fetch_top( } else { I80F48::ZERO }; - let perp_settle_health = - crate::health_cache::new(context, account_fetcher, &acc)?.perp_settle_health(); + let perp_settle_health = crate::health_cache::new(context, account_fetcher, &acc) + .await? + .perp_settle_health(); let settleable_pnl = if perp_settle_health > 0 && !acc.being_liquidated() { (*pnl).max(-perp_settle_health) } else { diff --git a/keeper/src/crank.rs b/keeper/src/crank.rs index 817aeb176..ba4f903ef 100644 --- a/keeper/src/crank.rs +++ b/keeper/src/crank.rs @@ -4,7 +4,6 @@ use crate::MangoClient; use itertools::Itertools; use anchor_lang::{__private::bytemuck::cast_ref, solana_program}; -use client::prettify_client_error; use futures::Future; use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, PerpMarket, TokenIndex}; use solana_sdk::{ @@ -93,79 +92,63 @@ pub async fn loop_update_index_and_rate( let token_indices_clone = token_indices.clone(); - let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { - let token_names = token_indices_clone + let token_names = token_indices_clone + .iter() + .map(|token_index| client.context.token(*token_index).name.to_owned()) + .join(","); + + let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_price(1)]; + for token_index in token_indices_clone.iter() { + let token = client.context.token(*token_index); + let banks_for_a_token = token.mint_info.banks(); + let oracle = token.mint_info.oracle; + + let mut ix = Instruction { + program_id: mango_v4::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::TokenUpdateIndexAndRate { + group: token.mint_info.group, + mint_info: token.mint_info_address, + oracle, + instructions: solana_program::sysvar::instructions::id(), + }, + None, + ), + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::TokenUpdateIndexAndRate {}, + ), + }; + let mut banks = banks_for_a_token .iter() - .map(|token_index| client.context.token(*token_index).name.to_owned()) - .join(","); + .map(|bank_pubkey| AccountMeta { + pubkey: *bank_pubkey, + is_signer: false, + is_writable: true, + }) + .collect::>(); + ix.accounts.append(&mut banks); + instructions.push(ix); + } + let pre = Instant::now(); + let sig_result = client + .send_and_confirm_permissionless_tx(instructions) + .await; - let program = client.program(); - let mut req = program.request(); - req = req.instruction(ComputeBudgetInstruction::set_compute_unit_price(1)); - for token_index in token_indices_clone.iter() { - let token = client.context.token(*token_index); - let banks_for_a_token = token.mint_info.banks(); - let oracle = token.mint_info.oracle; - - let mut ix = Instruction { - program_id: mango_v4::id(), - accounts: anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::TokenUpdateIndexAndRate { - group: token.mint_info.group, - mint_info: token.mint_info_address, - oracle, - instructions: solana_program::sysvar::instructions::id(), - }, - None, - ), - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::TokenUpdateIndexAndRate {}, - ), - }; - let mut banks = banks_for_a_token - .iter() - .map(|bank_pubkey| AccountMeta { - pubkey: *bank_pubkey, - is_signer: false, - is_writable: true, - }) - .collect::>(); - ix.accounts.append(&mut banks); - req = req.instruction(ix); - } - let pre = Instant::now(); - let sig_result = req.send().map_err(prettify_client_error); - - if let Err(e) = sig_result { - log::info!( - "metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}", - token_names, - pre.elapsed().as_millis(), - e - ); - log::error!("{:?}", e) - } else { - log::info!( - "metricName=UpdateTokensV4Success tokens={} durationMs={}", - token_names, - pre.elapsed().as_millis(), - ); - log::info!("{:?}", sig_result); - } - - Ok(()) - }) - .await; - - match res { - Ok(inner_res) => { - if inner_res.is_err() { - log::error!("{}", inner_res.unwrap_err()); - } - } - Err(join_error) => { - log::error!("{}", join_error); - } + if let Err(e) = sig_result { + log::info!( + "metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}", + token_names, + pre.elapsed().as_millis(), + e + ); + log::error!("{:?}", e) + } else { + log::info!( + "metricName=UpdateTokensV4Success tokens={} durationMs={}", + token_names, + pre.elapsed().as_millis(), + ); + log::info!("{:?}", sig_result); } } } @@ -181,11 +164,13 @@ pub async fn loop_consume_events( interval.tick().await; let client = mango_client.clone(); - let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { - let mut event_queue: EventQueue = - client.program().account(perp_market.event_queue).unwrap(); + let find_accounts = || async { let mut num_of_events = 0; + let mut event_queue: EventQueue = client + .client + .rpc_anchor_account(&perp_market.event_queue) + .await?; // TODO: future, choose better constant of how many max events to pack // TODO: future, choose better constant of how many max mango accounts to pack @@ -208,10 +193,28 @@ pub async fn loop_consume_events( EventType::Liquidate => {} } event_queue.pop_front()?; - num_of_events+=1; + num_of_events += 1; } - let mut ams_ = set + if num_of_events == 0 { + return Ok(None); + } + + Ok(Some((set, num_of_events))) + }; + + let event_info: anyhow::Result, u32)>> = find_accounts().await; + + let (event_accounts, num_of_events) = match event_info { + Ok(Some(x)) => x, + Ok(None) => continue, + Err(err) => { + log::error!("preparing consume_events ams: {err:?}"); + continue; + } + }; + + let mut event_ams = event_accounts .iter() .map(|key| -> AccountMeta { AccountMeta { @@ -222,67 +225,45 @@ pub async fn loop_consume_events( }) .collect::>(); - if num_of_events == 0 { - return Ok(()); - } - - let pre = Instant::now(); - let sig_result = client - .program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpConsumeEvents { - group: perp_market.group, - perp_market: pk, - event_queue: perp_market.event_queue, - }, - None, - ); - ams.append(&mut ams_); - ams + let pre = Instant::now(); + let ix = Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpConsumeEvents { + group: perp_market.group, + perp_market: pk, + event_queue: perp_market.event_queue, }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::PerpConsumeEvents { limit: 10 }, - ), - }) - .send() - .map_err(prettify_client_error); - - if let Err(e) = sig_result { - log::info!( - "metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}", - perp_market.name(), - pre.elapsed().as_millis(), - num_of_events, - e.to_string() + None, ); - log::error!("{:?}", e) - } else { - log::info!( - "metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}", - perp_market.name(), - pre.elapsed().as_millis(), - num_of_events, - ); - log::info!("{:?}", sig_result); - } + ams.append(&mut event_ams); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpConsumeEvents { + limit: 10, + }), + }; - Ok(()) - }) - .await; + let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).await; - match res { - Ok(inner_res) => { - if inner_res.is_err() { - log::error!("{}", inner_res.unwrap_err()); - } - } - Err(join_error) => { - log::error!("{}", join_error); - } + if let Err(e) = sig_result { + log::info!( + "metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}", + perp_market.name(), + pre.elapsed().as_millis(), + num_of_events, + e.to_string() + ); + log::error!("{:?}", e) + } else { + log::info!( + "metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}", + perp_market.name(), + pre.elapsed().as_millis(), + num_of_events, + ); + log::info!("{:?}", sig_result); } } } @@ -298,59 +279,39 @@ pub async fn loop_update_funding( interval.tick().await; let client = mango_client.clone(); - let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { - let pre = Instant::now(); - let sig_result = client - .program() - .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpUpdateFunding { - group: perp_market.group, - perp_market: pk, - bids: perp_market.bids, - asks: perp_market.asks, - oracle: perp_market.oracle, - }, - None, - ), - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::PerpUpdateFunding {}, - ), - }) - .send() - .map_err(prettify_client_error); - if let Err(e) = sig_result { - log::error!( - "metricName=UpdateFundingV4Error market={} durationMs={} error={}", - perp_market.name(), - pre.elapsed().as_millis(), - e.to_string() - ); - log::error!("{:?}", e) - } else { - log::info!( - "metricName=UpdateFundingV4Success market={} durationMs={}", - perp_market.name(), - pre.elapsed().as_millis(), - ); - log::info!("{:?}", sig_result); - } - Ok(()) - }) - .await; + let pre = Instant::now(); + let ix = Instruction { + program_id: mango_v4::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpUpdateFunding { + group: perp_market.group, + perp_market: pk, + bids: perp_market.bids, + asks: perp_market.asks, + oracle: perp_market.oracle, + }, + None, + ), + data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpUpdateFunding {}), + }; + let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).await; - match res { - Ok(inner_res) => { - if inner_res.is_err() { - log::error!("{}", inner_res.unwrap_err()); - } - } - Err(join_error) => { - log::error!("{}", join_error); - } + if let Err(e) = sig_result { + log::error!( + "metricName=UpdateFundingV4Error market={} durationMs={} error={}", + perp_market.name(), + pre.elapsed().as_millis(), + e.to_string() + ); + log::error!("{:?}", e) + } else { + log::info!( + "metricName=UpdateFundingV4Success market={} durationMs={}", + perp_market.name(), + pre.elapsed().as_millis(), + ); + log::info!("{:?}", sig_result); } } } diff --git a/keeper/src/main.rs b/keeper/src/main.rs index 988ab1305..4425b5b48 100644 --- a/keeper/src/main.rs +++ b/keeper/src/main.rs @@ -62,7 +62,9 @@ enum Command { Crank {}, Taker {}, } -fn main() -> Result<(), anyhow::Error> { + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { env_logger::init_from_env( env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), ); @@ -87,21 +89,19 @@ fn main() -> Result<(), anyhow::Error> { Command::Taker { .. } => CommitmentConfig::confirmed(), }; - let mango_client = Arc::new(MangoClient::new_for_existing_account( - Client::new( - cluster, - commitment, - &owner, - Some(Duration::from_secs(cli.timeout)), - ), - cli.mango_account, - owner, - )?); - - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); + let mango_client = Arc::new( + MangoClient::new_for_existing_account( + Client::new( + cluster, + commitment, + &owner, + Some(Duration::from_secs(cli.timeout)), + ), + cli.mango_account, + owner, + ) + .await?, + ); let debugging_handle = async { let mut interval = time::interval(time::Duration::from_secs(5)); @@ -120,17 +120,18 @@ fn main() -> Result<(), anyhow::Error> { match cli.command { Command::Crank { .. } => { let client = mango_client.clone(); - rt.block_on(crank::runner( + crank::runner( client, debugging_handle, cli.interval_update_banks, cli.interval_consume_events, cli.interval_update_funding, - )) + ) + .await } Command::Taker { .. } => { let client = mango_client.clone(); - rt.block_on(taker::runner(client, debugging_handle)) + taker::runner(client, debugging_handle).await } } } diff --git a/keeper/src/taker.rs b/keeper/src/taker.rs index d6919e8a5..c9806ac5e 100644 --- a/keeper/src/taker.rs +++ b/keeper/src/taker.rs @@ -16,9 +16,8 @@ pub async fn runner( mango_client: Arc, _debugging_handle: impl Future, ) -> Result<(), anyhow::Error> { - ensure_deposit(&mango_client)?; - - ensure_oo(&mango_client)?; + ensure_deposit(&mango_client).await?; + ensure_oo(&mango_client).await?; let mut price_arcs = HashMap::new(); for market_name in mango_client.context.serum3_market_indexes_by_name.keys() { @@ -30,6 +29,7 @@ pub async fn runner( .first() .unwrap(), ) + .await .unwrap(); price_arcs.insert( market_name.to_owned(), @@ -73,23 +73,25 @@ pub async fn runner( Ok(()) } -fn ensure_oo(mango_client: &Arc) -> Result<(), anyhow::Error> { - let account = mango_client.mango_account()?; +async fn ensure_oo(mango_client: &Arc) -> Result<(), anyhow::Error> { + let account = mango_client.mango_account().await?; for (market_index, serum3_market) in mango_client.context.serum3_markets.iter() { if account.serum3_orders(*market_index).is_err() { - mango_client.serum3_create_open_orders(serum3_market.market.name())?; + mango_client + .serum3_create_open_orders(serum3_market.market.name()) + .await?; } } Ok(()) } -fn ensure_deposit(mango_client: &Arc) -> Result<(), anyhow::Error> { - let mango_account = mango_client.mango_account()?; +async fn ensure_deposit(mango_client: &Arc) -> Result<(), anyhow::Error> { + let mango_account = mango_client.mango_account().await?; for &token_index in mango_client.context.tokens.keys() { - let bank = mango_client.first_bank(token_index)?; + let bank = mango_client.first_bank(token_index).await?; let desired_balance = I80F48::from_num(10_000 * 10u64.pow(bank.mint_decimals as u32)); let token_account_opt = mango_account.token_position(token_index).ok(); @@ -114,7 +116,9 @@ fn ensure_deposit(mango_client: &Arc) -> Result<(), anyhow::Error> } log::info!("Depositing {} {}", deposit_native, bank.name()); - mango_client.token_deposit(bank.mint, desired_balance.to_num())?; + mango_client + .token_deposit(bank.mint, desired_balance.to_num()) + .await?; } Ok(()) @@ -126,33 +130,15 @@ pub async fn loop_blocking_price_update( price: Arc>, ) { let mut interval = time::interval(Duration::from_secs(1)); + let token_name = market_name.split('/').collect::>()[0]; loop { interval.tick().await; - let client1 = mango_client.clone(); - let market_name1 = market_name.clone(); - let price = price.clone(); - let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { - let token_name = market_name1.split('/').collect::>()[0]; - let fresh_price = client1.get_oracle_price(token_name).unwrap(); - log::info!("{} Updated price is {:?}", token_name, fresh_price.price); - if let Ok(mut price) = price.write() { - *price = I80F48::from_num(fresh_price.price) - / I80F48::from_num(10u64.pow(-fresh_price.expo as u32)); - } - Ok(()) - }) - .await; - - match res { - Ok(inner_res) => { - if inner_res.is_err() { - log::error!("{}", inner_res.unwrap_err()); - } - } - Err(join_error) => { - log::error!("{}", join_error); - } + let fresh_price = mango_client.get_oracle_price(token_name).await.unwrap(); + log::info!("{} Updated price is {:?}", token_name, fresh_price.price); + if let Ok(mut price) = price.write() { + *price = I80F48::from_num(fresh_price.price) + / I80F48::from_num(10u64.pow(-fresh_price.expo as u32)); } } } @@ -165,7 +151,10 @@ pub async fn loop_blocking_orders( let mut interval = time::interval(Duration::from_secs(5)); // Cancel existing orders - let orders: Vec = mango_client.serum3_cancel_all_orders(&market_name).unwrap(); + let orders: Vec = mango_client + .serum3_cancel_all_orders(&market_name) + .await + .unwrap(); log::info!("Cancelled orders - {:?} for {}", orders, market_name); loop { @@ -175,8 +164,8 @@ pub async fn loop_blocking_orders( let market_name = market_name.clone(); let price = price.clone(); - let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { - client.serum3_settle_funds(&market_name)?; + let res = (|| async move { + client.serum3_settle_funds(&market_name).await?; let fresh_price = match price.read() { Ok(price) => *price, @@ -188,16 +177,18 @@ pub async fn loop_blocking_orders( let fresh_price = fresh_price.to_num::(); let bid_price = fresh_price + fresh_price * 0.1; - let res = client.serum3_place_order( - &market_name, - Serum3Side::Bid, - bid_price, - 0.0001, - Serum3SelfTradeBehavior::DecrementTake, - Serum3OrderType::ImmediateOrCancel, - SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64, - 10, - ); + let res = client + .serum3_place_order( + &market_name, + Serum3Side::Bid, + bid_price, + 0.0001, + Serum3SelfTradeBehavior::DecrementTake, + Serum3OrderType::ImmediateOrCancel, + SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64, + 10, + ) + .await; if let Err(e) = res { log::error!("Error while placing taker bid {:#?}", e) } else { @@ -205,16 +196,18 @@ pub async fn loop_blocking_orders( } let ask_price = fresh_price - fresh_price * 0.1; - let res = client.serum3_place_order( - &market_name, - Serum3Side::Ask, - ask_price, - 0.0001, - Serum3SelfTradeBehavior::DecrementTake, - Serum3OrderType::ImmediateOrCancel, - SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64, - 10, - ); + let res = client + .serum3_place_order( + &market_name, + Serum3Side::Ask, + ask_price, + 0.0001, + Serum3SelfTradeBehavior::DecrementTake, + Serum3OrderType::ImmediateOrCancel, + SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64, + 10, + ) + .await; if let Err(e) = res { log::error!("Error while placing taker ask {:#?}", e) } else { @@ -222,18 +215,11 @@ pub async fn loop_blocking_orders( } Ok(()) - }) + })() .await; - match res { - Ok(inner_res) => { - if inner_res.is_err() { - log::error!("{}", inner_res.unwrap_err()); - } - } - Err(join_error) => { - log::error!("{}", join_error); - } + if let Err(err) = res { + log::error!("{:?}", err); } } } diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index aacec1cb5..ffc7a7834 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -1,14 +1,15 @@ +use std::collections::HashSet; use std::time::Duration; use client::{chain_data, health_cache, AccountFetcher, MangoClient, MangoClientError}; use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::health::{HealthCache, HealthType}; use mango_v4::state::{ - Bank, MangoAccountValue, PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX, + Bank, MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX, }; use solana_sdk::signature::Signature; -use itertools::Itertools; +use futures::{stream, StreamExt, TryStreamExt}; use rand::seq::SliceRandom; use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey}; @@ -17,7 +18,7 @@ pub struct Config { pub refresh_timeout: Duration, } -pub fn jupiter_market_can_buy( +pub async fn jupiter_market_can_buy( mango_client: &MangoClient, token: TokenIndex, quote_token: TokenIndex, @@ -41,10 +42,11 @@ pub fn jupiter_market_can_buy( slippage, client::JupiterSwapMode::ExactIn, ) + .await .is_ok() } -pub fn jupiter_market_can_sell( +pub async fn jupiter_market_can_sell( mango_client: &MangoClient, token: TokenIndex, quote_token: TokenIndex, @@ -68,6 +70,7 @@ pub fn jupiter_market_can_sell( slippage, client::JupiterSwapMode::ExactOut, ) + .await .is_ok() } @@ -79,40 +82,50 @@ struct LiquidateHelper<'a> { health_cache: &'a HealthCache, maint_health: I80F48, liqor_min_health_ratio: I80F48, + allowed_asset_tokens: HashSet, + allowed_liab_tokens: HashSet, } impl<'a> LiquidateHelper<'a> { - fn serum3_close_orders(&self) -> anyhow::Result> { + async fn serum3_close_orders(&self) -> anyhow::Result> { // look for any open serum orders or settleable balances - let serum_force_cancels = self - .liqee - .active_serum3_orders() - .map(|orders| { + let serum_oos: anyhow::Result> = stream::iter(self.liqee.active_serum3_orders()) + .then(|orders| async { let open_orders_account = self .account_fetcher - .fetch_raw_account(&orders.open_orders)?; + .fetch_raw_account(&orders.open_orders) + .await?; let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?; + Ok((*orders, *open_orders)) + }) + .try_collect() + .await; + let serum_force_cancels = serum_oos? + .into_iter() + .filter_map(|(orders, open_orders)| { let can_force_cancel = open_orders.native_coin_total > 0 || open_orders.native_pc_total > 0 || open_orders.referrer_rebates_accrued > 0; if can_force_cancel { - Ok(Some(*orders)) + Some(orders) } else { - Ok(None) + None } }) - .filter_map_ok(|v| v) - .collect::>>()?; + .collect::>(); if serum_force_cancels.is_empty() { return Ok(None); } // Cancel all orders on a random serum market let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap(); - let sig = self.client.serum3_liq_force_cancel_orders( - (self.pubkey, &self.liqee), - serum_orders.market_index, - &serum_orders.open_orders, - )?; + let sig = self + .client + .serum3_liq_force_cancel_orders( + (self.pubkey, &self.liqee), + serum_orders.market_index, + &serum_orders.open_orders, + ) + .await?; log::info!( "Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}", self.pubkey, @@ -123,7 +136,7 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn perp_close_orders(&self) -> anyhow::Result> { + async fn perp_close_orders(&self) -> anyhow::Result> { let perp_force_cancels = self .liqee .active_perp_positions() @@ -137,7 +150,8 @@ impl<'a> LiquidateHelper<'a> { let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap(); let sig = self .client - .perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index)?; + .perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index) + .await?; log::info!( "Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}", self.pubkey, @@ -148,11 +162,11 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn perp_liq_base_position(&self) -> anyhow::Result> { - let mut perp_base_positions = self - .liqee - .active_perp_positions() - .map(|pp| { + async fn perp_liq_base_position(&self) -> anyhow::Result> { + let all_perp_base_positions: anyhow::Result< + Vec>, + > = stream::iter(self.liqee.active_perp_positions()) + .then(|pp| async { let base_lots = pp.base_position_lots(); if base_lots == 0 { return Ok(None); @@ -160,7 +174,8 @@ impl<'a> LiquidateHelper<'a> { let perp = self.client.context.perp(pp.market_index); let oracle = self .account_fetcher - .fetch_raw_account(&perp.market.oracle)?; + .fetch_raw_account(&perp.market.oracle) + .await?; let price = perp.market.oracle_price( &KeyedAccountSharedData::new(perp.market.oracle, oracle.into()), None, @@ -172,8 +187,12 @@ impl<'a> LiquidateHelper<'a> { I80F48::from(base_lots.abs()) * price, ))) }) - .filter_map_ok(|v| v) - .collect::>>()?; + .try_collect() + .await; + let mut perp_base_positions = all_perp_base_positions? + .into_iter() + .filter_map(|x| x) + .collect::>(); perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3)); if perp_base_positions.is_empty() { @@ -194,10 +213,12 @@ impl<'a> LiquidateHelper<'a> { let mut liqor = self .account_fetcher .fetch_fresh_mango_account(&self.client.mango_account_address) + .await .context("getting liquidator account")?; liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?; let health_cache = health_cache::new(&self.client.context, self.account_fetcher, &liqor) + .await .expect("always ok"); health_cache.max_perp_for_health_ratio( *perp_market_index, @@ -208,11 +229,14 @@ impl<'a> LiquidateHelper<'a> { }; log::info!("computed max_base_transfer to be {max_base_transfer_abs}"); - let sig = self.client.perp_liq_base_position( - (self.pubkey, &self.liqee), - *perp_market_index, - side_signum * max_base_transfer_abs, - )?; + let sig = self + .client + .perp_liq_base_position( + (self.pubkey, &self.liqee), + *perp_market_index, + side_signum * max_base_transfer_abs, + ) + .await?; log::info!( "Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}", self.pubkey, @@ -223,7 +247,7 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn perp_settle_pnl(&self) -> anyhow::Result> { + async fn perp_settle_pnl(&self) -> anyhow::Result> { let perp_settle_health = self.health_cache.perp_settle_health(); let mut perp_settleable_pnl = self .liqee @@ -262,7 +286,8 @@ impl<'a> LiquidateHelper<'a> { perp_index, direction, 2, - )?; + ) + .await?; if counters.is_empty() { // If we can't settle some positive PNL because we're lacking a suitable counterparty, // then liquidation should continue, even though this step produced no transaction @@ -270,7 +295,9 @@ impl<'a> LiquidateHelper<'a> { self.pubkey); continue; } - let (counter_key, counter_acc, _) = counters.first().unwrap(); + let (counter_key, counter_acc, counter_pnl) = counters.first().unwrap(); + + log::info!("Trying to settle perp pnl account: {} market_index: {perp_index} amount: {pnl} against {counter_key} with pnl: {counter_pnl}", self.pubkey); let (account_a, account_b) = if pnl > 0 { ((self.pubkey, self.liqee), (counter_key, counter_acc)) @@ -279,7 +306,8 @@ impl<'a> LiquidateHelper<'a> { }; let sig = self .client - .perp_settle_pnl(perp_index, account_a, account_b)?; + .perp_settle_pnl(perp_index, account_a, account_b) + .await?; log::info!( "Settled perp pnl for perp market on account {}, market index {perp_index}, maint_health was {}, tx sig {sig:?}", self.pubkey, @@ -290,7 +318,7 @@ impl<'a> LiquidateHelper<'a> { return Ok(None); } - fn perp_liq_bankruptcy(&self) -> anyhow::Result> { + async fn perp_liq_bankruptcy(&self) -> anyhow::Result> { if self.health_cache.has_liquidatable_assets() { return Ok(None); } @@ -312,12 +340,15 @@ impl<'a> LiquidateHelper<'a> { } let (perp_market_index, _) = perp_bankruptcies.first().unwrap(); - let sig = self.client.perp_liq_bankruptcy( - (self.pubkey, &self.liqee), - *perp_market_index, - // Always use the max amount, since the health effect is always positive - u64::MAX, - )?; + let sig = self + .client + .perp_liq_bankruptcy( + (self.pubkey, &self.liqee), + *perp_market_index, + // Always use the max amount, since the health effect is always positive + u64::MAX, + ) + .await?; log::info!( "Liquidated bankruptcy for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}", self.pubkey, @@ -328,34 +359,36 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn tokens(&self) -> anyhow::Result> { - let mut tokens = self - .liqee - .active_token_positions() - .map(|token_position| { - let token = self.client.context.token(token_position.token_index); - let bank = self - .account_fetcher - .fetch::(&token.mint_info.first_bank())?; - let oracle = self - .account_fetcher - .fetch_raw_account(&token.mint_info.oracle)?; - let price = bank.oracle_price( - &KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()), - None, - )?; - Ok(( - token_position.token_index, - price, - token_position.native(&bank) * price, - )) - }) - .collect::>>()?; + async fn tokens(&self) -> anyhow::Result> { + let tokens_maybe: anyhow::Result> = + stream::iter(self.liqee.active_token_positions()) + .then(|token_position| async { + let token = self.client.context.token(token_position.token_index); + let bank = self + .account_fetcher + .fetch::(&token.mint_info.first_bank())?; + let oracle = self + .account_fetcher + .fetch_raw_account(&token.mint_info.oracle) + .await?; + let price = bank.oracle_price( + &KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()), + None, + )?; + Ok(( + token_position.token_index, + price, + token_position.native(&bank) * price, + )) + }) + .try_collect() + .await; + let mut tokens = tokens_maybe?; tokens.sort_by(|a, b| a.2.cmp(&b.2)); Ok(tokens) } - fn max_token_liab_transfer( + async fn max_token_liab_transfer( &self, source: TokenIndex, target: TokenIndex, @@ -363,6 +396,7 @@ impl<'a> LiquidateHelper<'a> { let mut liqor = self .account_fetcher .fetch_fresh_mango_account(&self.client.mango_account_address) + .await .context("getting liquidator account")?; // Ensure the tokens are activated, so they appear in the health cache and @@ -371,10 +405,11 @@ impl<'a> LiquidateHelper<'a> { liqor.ensure_token_position(target)?; let health_cache = health_cache::new(&self.client.context, self.account_fetcher, &liqor) + .await .expect("always ok"); - let source_bank = self.client.first_bank(source)?; - let target_bank = self.client.first_bank(target)?; + let source_bank = self.client.first_bank(source).await?; + let target_bank = self.client.first_bank(target).await?; let source_price = health_cache.token_info(source).unwrap().prices.oracle; let target_price = health_cache.token_info(target).unwrap().prices.oracle; @@ -394,19 +429,21 @@ impl<'a> LiquidateHelper<'a> { Ok(amount) } - fn token_liq(&self) -> anyhow::Result> { - if !self.health_cache.has_borrows() || self.health_cache.can_call_spot_bankruptcy() { + async fn token_liq(&self) -> anyhow::Result> { + if !self.health_cache.has_spot_assets() || !self.health_cache.has_spot_borrows() { return Ok(None); } - let tokens = self.tokens()?; + let tokens = self.tokens().await?; let asset_token_index = tokens .iter() .rev() .find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| { asset_usdc_equivalent.is_positive() - && jupiter_market_can_sell(self.client, *asset_token_index, QUOTE_TOKEN_INDEX) + && self + .allowed_asset_tokens + .contains(&self.client.context.token(*asset_token_index).mint_info.mint) }) .ok_or_else(|| { anyhow::anyhow!( @@ -420,7 +457,9 @@ impl<'a> LiquidateHelper<'a> { .iter() .find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| { liab_usdc_equivalent.is_negative() - && jupiter_market_can_buy(self.client, *liab_token_index, QUOTE_TOKEN_INDEX) + && self + .allowed_liab_tokens + .contains(&self.client.context.token(*liab_token_index).mint_info.mint) }) .ok_or_else(|| { anyhow::anyhow!( @@ -433,6 +472,7 @@ impl<'a> LiquidateHelper<'a> { let max_liab_transfer = self .max_token_liab_transfer(liab_token_index, asset_token_index) + .await .context("getting max_liab_transfer")?; // @@ -447,6 +487,7 @@ impl<'a> LiquidateHelper<'a> { liab_token_index, max_liab_transfer, ) + .await .context("sending liq_token_with_token")?; log::info!( "Liquidated token with token for {}, maint_health was {}, tx sig {:?}", @@ -457,12 +498,12 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn token_liq_bankruptcy(&self) -> anyhow::Result> { + async fn token_liq_bankruptcy(&self) -> anyhow::Result> { if !self.health_cache.can_call_spot_bankruptcy() { return Ok(None); } - let tokens = self.tokens()?; + let tokens = self.tokens().await?; if tokens.is_empty() { anyhow::bail!( @@ -474,7 +515,9 @@ impl<'a> LiquidateHelper<'a> { .iter() .find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| { liab_usdc_equivalent.is_negative() - && jupiter_market_can_buy(self.client, *liab_token_index, QUOTE_TOKEN_INDEX) + && self + .allowed_liab_tokens + .contains(&self.client.context.token(*liab_token_index).mint_info.mint) }) .ok_or_else(|| { anyhow::anyhow!( @@ -486,8 +529,9 @@ impl<'a> LiquidateHelper<'a> { .0; let quote_token_index = 0; - let max_liab_transfer = - self.max_token_liab_transfer(liab_token_index, quote_token_index)?; + let max_liab_transfer = self + .max_token_liab_transfer(liab_token_index, quote_token_index) + .await?; let sig = self .client @@ -496,6 +540,7 @@ impl<'a> LiquidateHelper<'a> { liab_token_index, max_liab_transfer, ) + .await .context("sending liq_token_bankruptcy")?; log::info!( "Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}", @@ -506,7 +551,7 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn send_liq_tx(&self) -> anyhow::Result { + async fn send_liq_tx(&self) -> anyhow::Result { // TODO: Should we make an attempt to settle positive PNL first? // The problem with it is that small market movements can continuously create // small amounts of new positive PNL while base_position > 0. @@ -517,14 +562,14 @@ impl<'a> LiquidateHelper<'a> { // } // Try to close orders before touching the user's positions - if let Some(txsig) = self.perp_close_orders()? { + if let Some(txsig) = self.perp_close_orders().await? { return Ok(txsig); } - if let Some(txsig) = self.serum3_close_orders()? { + if let Some(txsig) = self.serum3_close_orders().await? { return Ok(txsig); } - if let Some(txsig) = self.perp_liq_base_position()? { + if let Some(txsig) = self.perp_liq_base_position().await? { return Ok(txsig); } @@ -533,21 +578,21 @@ impl<'a> LiquidateHelper<'a> { // It's possible that some positive pnl can't be settled (if there's // no liquid counterparty) and that some negative pnl can't be settled // (if the liqee isn't liquid enough). - if let Some(txsig) = self.perp_settle_pnl()? { + if let Some(txsig) = self.perp_settle_pnl().await? { return Ok(txsig); } - if let Some(txsig) = self.token_liq()? { + if let Some(txsig) = self.token_liq().await? { return Ok(txsig); } // Socialize/insurance fund unsettleable negative pnl - if let Some(txsig) = self.perp_liq_bankruptcy()? { + if let Some(txsig) = self.perp_liq_bankruptcy().await? { return Ok(txsig); } // Socialize/insurance fund unliquidatable borrows - if let Some(txsig) = self.token_liq_bankruptcy()? { + if let Some(txsig) = self.token_liq_bankruptcy().await? { return Ok(txsig); } @@ -562,7 +607,7 @@ impl<'a> LiquidateHelper<'a> { } #[allow(clippy::too_many_arguments)] -pub fn maybe_liquidate_account( +pub async fn maybe_liquidate_account( mango_client: &MangoClient, account_fetcher: &chain_data::AccountFetcher, pubkey: &Pubkey, @@ -571,8 +616,9 @@ pub fn maybe_liquidate_account( let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio); let account = account_fetcher.fetch_mango_account(pubkey)?; - let health_cache = - health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok"); + let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account) + .await + .expect("always ok"); let maint_health = health_cache.health(HealthType::Maint); if !health_cache.is_liquidatable() { return Ok(false); @@ -588,15 +634,24 @@ pub fn maybe_liquidate_account( // Fetch a fresh account and re-compute // This is -- unfortunately -- needed because the websocket streams seem to not // be great at providing timely updates to the account data. - let account = account_fetcher.fetch_fresh_mango_account(pubkey)?; - let health_cache = - health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok"); + let account = account_fetcher.fetch_fresh_mango_account(pubkey).await?; + let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account) + .await + .expect("always ok"); if !health_cache.is_liquidatable() { return Ok(false); } let maint_health = health_cache.health(HealthType::Maint); + let all_token_mints = HashSet::from_iter( + mango_client + .context + .tokens + .values() + .map(|c| c.mint_info.mint), + ); + // try liquidating let txsig = LiquidateHelper { client: mango_client, @@ -606,15 +661,21 @@ pub fn maybe_liquidate_account( health_cache: &health_cache, maint_health, liqor_min_health_ratio, + allowed_asset_tokens: all_token_mints.clone(), + allowed_liab_tokens: all_token_mints, } - .send_liq_tx()?; + .send_liq_tx() + .await?; - let slot = account_fetcher.transaction_max_slot(&[txsig])?; - if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot( - &[*pubkey, mango_client.mango_account_address], - slot, - config.refresh_timeout, - ) { + let slot = account_fetcher.transaction_max_slot(&[txsig]).await?; + if let Err(e) = account_fetcher + .refresh_accounts_via_rpc_until_slot( + &[*pubkey, mango_client.mango_account_address], + slot, + config.refresh_timeout, + ) + .await + { log::info!("could not refresh after liquidation: {}", e); } @@ -622,14 +683,14 @@ pub fn maybe_liquidate_account( } #[allow(clippy::too_many_arguments)] -pub fn maybe_liquidate_one<'a>( +pub async fn maybe_liquidate_one<'a>( mango_client: &MangoClient, account_fetcher: &chain_data::AccountFetcher, accounts: impl Iterator, config: &Config, ) -> bool { for pubkey in accounts { - match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config) { + match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config).await { Err(err) => { // Not all errors need to be raised to the user's attention. let mut log_level = log::Level::Error; diff --git a/liquidator/src/main.rs b/liquidator/src/main.rs index 93be96045..64be4563a 100644 --- a/liquidator/src/main.rs +++ b/liquidator/src/main.rs @@ -114,13 +114,15 @@ async fn main() -> anyhow::Result<()> { // Reading accounts from chain_data let account_fetcher = Arc::new(chain_data::AccountFetcher { chain_data: chain_data.clone(), - rpc: client.rpc(), + rpc: client.rpc_async(), }); - let mango_account = account_fetcher.fetch_fresh_mango_account(&cli.liqor_mango_account)?; + let mango_account = account_fetcher + .fetch_fresh_mango_account(&cli.liqor_mango_account) + .await?; let mango_group = mango_account.fixed.group; - let group_context = MangoGroupContext::new_from_rpc(mango_group, cluster.clone(), commitment)?; + let group_context = MangoGroupContext::new_from_rpc(&client.rpc_async(), mango_group).await?; let mango_oracles = group_context .tokens @@ -258,7 +260,7 @@ async fn main() -> anyhow::Result<()> { std::iter::once(&account_write.pubkey), &liq_config, &rebalance_config, - )?; + ).await?; } if is_mango_bank(&account_write.account, &mango_program, &mango_group).is_some() || oracles.contains(&account_write.pubkey) { @@ -280,7 +282,7 @@ async fn main() -> anyhow::Result<()> { mango_accounts.iter(), &liq_config, &rebalance_config, - )?; + ).await?; } } }, @@ -313,12 +315,12 @@ async fn main() -> anyhow::Result<()> { mango_accounts.iter(), &liq_config, &rebalance_config, - )?; + ).await?; }, _ = rebalance_interval.tick() => { if one_snapshot_done { - if let Err(err) = rebalance::zero_all_non_quote(&mango_client, &account_fetcher, &cli.liqor_mango_account, &rebalance_config) { + if let Err(err) = rebalance::zero_all_non_quote(&mango_client, &account_fetcher, &cli.liqor_mango_account, &rebalance_config).await { log::error!("failed to rebalance liqor: {:?}", err); // Workaround: We really need a sequence enforcer in the liquidator since we don't want to @@ -332,20 +334,20 @@ async fn main() -> anyhow::Result<()> { } } -fn liquidate<'a>( +async fn liquidate<'a>( mango_client: &MangoClient, account_fetcher: &chain_data::AccountFetcher, accounts: impl Iterator, config: &liquidate::Config, rebalance_config: &rebalance::Config, ) -> anyhow::Result<()> { - if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config) { + if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config).await { return Ok(()); } let liqor = &mango_client.mango_account_address; if let Err(err) = - rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config) + rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config).await { log::error!("failed to rebalance liqor: {:?}", err); } diff --git a/liquidator/src/rebalance.rs b/liquidator/src/rebalance.rs index ef048202b..63eb1d0cd 100644 --- a/liquidator/src/rebalance.rs +++ b/liquidator/src/rebalance.rs @@ -6,6 +6,7 @@ use mango_v4::state::{Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX}; use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey}; +use futures::{stream, StreamExt, TryStreamExt}; use std::{collections::HashMap, time::Duration}; pub struct Config { @@ -20,14 +21,14 @@ struct TokenState { } impl TokenState { - fn new_position( + async fn new_position( token: &TokenContext, position: &TokenPosition, account_fetcher: &chain_data::AccountFetcher, ) -> anyhow::Result { let bank = Self::bank(token, account_fetcher)?; Ok(Self { - price: Self::fetch_price(token, &bank, account_fetcher)?, + price: Self::fetch_price(token, &bank, account_fetcher).await?, native_position: position.native(&bank), }) } @@ -39,12 +40,14 @@ impl TokenState { account_fetcher.fetch::(&token.mint_info.first_bank()) } - fn fetch_price( + async fn fetch_price( token: &TokenContext, bank: &Bank, account_fetcher: &chain_data::AccountFetcher, ) -> anyhow::Result { - let oracle = account_fetcher.fetch_raw_account(&token.mint_info.oracle)?; + let oracle = account_fetcher + .fetch_raw_account(&token.mint_info.oracle) + .await?; bank.oracle_price( &KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()), None, @@ -54,7 +57,7 @@ impl TokenState { } #[allow(clippy::too_many_arguments)] -pub fn zero_all_non_quote( +pub async fn zero_all_non_quote( mango_client: &MangoClient, account_fetcher: &chain_data::AccountFetcher, mango_account_address: &Pubkey, @@ -67,27 +70,32 @@ pub fn zero_all_non_quote( let account = account_fetcher.fetch_mango_account(mango_account_address)?; - let tokens = account - .active_token_positions() - .map(|token_position| { - let token = mango_client.context.token(token_position.token_index); - Ok(( - token.token_index, - TokenState::new_position(token, token_position, account_fetcher)?, - )) - }) - .collect::>>()?; + let tokens: anyhow::Result> = + stream::iter(account.active_token_positions()) + .then(|token_position| async { + let token = mango_client.context.token(token_position.token_index); + Ok(( + token.token_index, + TokenState::new_position(token, token_position, account_fetcher).await?, + )) + }) + .try_collect() + .await; + let tokens = tokens?; log::trace!("account tokens: {:?}", tokens); // Function to refresh the mango account after the txsig confirmed. Returns false on timeout. - let refresh_mango_account = - |account_fetcher: &chain_data::AccountFetcher, txsig| -> anyhow::Result { - let max_slot = account_fetcher.transaction_max_slot(&[txsig])?; - if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot( - &[*mango_account_address], - max_slot, - config.refresh_timeout, - ) { + let refresh_mango_account = |txsig| async move { + let res: anyhow::Result = { + let max_slot = account_fetcher.transaction_max_slot(&[txsig]).await?; + if let Err(e) = account_fetcher + .refresh_accounts_via_rpc_until_slot( + &[*mango_account_address], + max_slot, + config.refresh_timeout, + ) + .await + { // If we don't get fresh data, maybe the tx landed on a fork? // Rebalance is technically still ok. log::info!("could not refresh account data: {}", e); @@ -95,6 +103,8 @@ pub fn zero_all_non_quote( } Ok(true) }; + res + }; for (token_index, token_state) in tokens { let token = mango_client.context.token(token_index); @@ -120,13 +130,15 @@ pub fn zero_all_non_quote( if amount > dust_threshold { // Sell - let txsig = mango_client.jupiter_swap( - token_mint, - quote_mint, - amount.to_num::(), - config.slippage, - client::JupiterSwapMode::ExactIn, - )?; + let txsig = mango_client + .jupiter_swap( + token_mint, + quote_mint, + amount.to_num::(), + config.slippage, + client::JupiterSwapMode::ExactIn, + ) + .await?; log::info!( "sold {} {} for {} in tx {}", token.native_to_ui(amount), @@ -134,12 +146,13 @@ pub fn zero_all_non_quote( quote_token.name, txsig ); - if !refresh_mango_account(account_fetcher, txsig)? { + if !refresh_mango_account(txsig).await? { return Ok(()); } let bank = TokenState::bank(token, account_fetcher)?; amount = mango_client - .mango_account()? + .mango_account() + .await? .token_position_and_raw_index(token_index) .map(|(position, _)| position.native(&bank)) .unwrap_or(I80F48::ZERO); @@ -147,13 +160,15 @@ pub fn zero_all_non_quote( // Buy let buy_amount = (-token_state.native_position).ceil() + (dust_threshold - I80F48::ONE).max(I80F48::ZERO); - let txsig = mango_client.jupiter_swap( - quote_mint, - token_mint, - buy_amount.to_num::(), - config.slippage, - client::JupiterSwapMode::ExactOut, - )?; + let txsig = mango_client + .jupiter_swap( + quote_mint, + token_mint, + buy_amount.to_num::(), + config.slippage, + client::JupiterSwapMode::ExactOut, + ) + .await?; log::info!( "bought {} {} for {} in tx {}", token.native_to_ui(buy_amount), @@ -161,12 +176,13 @@ pub fn zero_all_non_quote( quote_token.name, txsig ); - if !refresh_mango_account(account_fetcher, txsig)? { + if !refresh_mango_account(txsig).await? { return Ok(()); } let bank = TokenState::bank(token, account_fetcher)?; amount = mango_client - .mango_account()? + .mango_account() + .await? .token_position_and_raw_index(token_index) .map(|(position, _)| position.native(&bank)) .unwrap_or(I80F48::ZERO); @@ -176,15 +192,16 @@ pub fn zero_all_non_quote( // TokenPosition is freed up if amount > 0 && amount <= dust_threshold { let allow_borrow = false; - let txsig = - mango_client.token_withdraw(token_mint, amount.to_num::(), allow_borrow)?; + let txsig = mango_client + .token_withdraw(token_mint, amount.to_num::(), allow_borrow) + .await?; log::info!( "withdrew {} {} to liqor wallet in {}", token.native_to_ui(amount), token.name, txsig ); - if !refresh_mango_account(account_fetcher, txsig)? { + if !refresh_mango_account(txsig).await? { return Ok(()); } } else if amount > dust_threshold { diff --git a/liquidator/src/websocket_source.rs b/liquidator/src/websocket_source.rs index 088161d18..5132af795 100644 --- a/liquidator/src/websocket_source.rs +++ b/liquidator/src/websocket_source.rs @@ -175,7 +175,10 @@ pub fn start(config: Config, mango_oracles: Vec, sender: async_channel:: loop { info!("connecting to solana websocket streams"); let out = feed_data(&config, mango_oracles.clone(), sender.clone()); - let _ = out.await; + let result = out.await; + if let Err(err) = result { + warn!("websocket stream error: {err}"); + } } }); } diff --git a/programs/mango-v4/src/health/cache.rs b/programs/mango-v4/src/health/cache.rs index e21c0fffd..b5ecd9b9b 100644 --- a/programs/mango-v4/src/health/cache.rs +++ b/programs/mango-v4/src/health/cache.rs @@ -1,7 +1,6 @@ use anchor_lang::prelude::*; use fixed::types::I80F48; -use fixed_macro::types::I80F48; use crate::error::*; use crate::state::{ @@ -12,8 +11,6 @@ use crate::util::checked_math as cm; use super::*; -const ONE_NATIVE_USDC_IN_USD: I80F48 = I80F48!(0.000001); - /// Information about prices for a bank or perp market. #[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)] pub struct Prices { @@ -442,11 +439,15 @@ impl HealthCache { Ok(()) } - pub fn has_liquidatable_assets(&self) -> bool { - let spot_liquidatable = self.token_infos.iter().any(|ti| { + pub fn has_spot_assets(&self) -> bool { + self.token_infos.iter().any(|ti| { // can use token_liq_with_token ti.balance_native.is_positive() - }); + }) + } + + pub fn has_liquidatable_assets(&self) -> bool { + let spot_liquidatable = self.has_spot_assets(); // can use serum3_liq_force_cancel_orders let serum3_cancelable = self .serum3_infos @@ -455,10 +456,11 @@ impl HealthCache { let perp_liquidatable = self.perp_infos.iter().any(|p| { // can use perp_liq_base_position p.base_lots != 0 - // can use perp_settle_pnl - || p.quote > ONE_NATIVE_USDC_IN_USD // TODO: we're not guaranteed to be able to settle positive perp pnl! // can use perp_liq_force_cancel_orders || p.has_open_orders + // A remaining quote position can be reduced with perp_settle_pnl and that can improve health. + // However, since it's not guaranteed that there is a counterparty, a positive perp quote position + // does not prevent bankruptcy. }); spot_liquidatable || serum3_cancelable || perp_liquidatable } diff --git a/ts/client/src/scripts/mb-admin-close.ts b/ts/client/src/scripts/mb-admin-close.ts index f57b0d4ac..6d7494ca0 100644 --- a/ts/client/src/scripts/mb-admin-close.ts +++ b/ts/client/src/scripts/mb-admin-close.ts @@ -67,7 +67,6 @@ async function main() { // close all banks for (const banks of group.banksMapByMint.values()) { - console.log(banks[0].toString()); sig = await client.tokenDeregister(group, banks[0].mint); console.log( `Removed token ${banks[0].name}, sig https://explorer.solana.com/tx/${sig}`, diff --git a/ts/client/src/scripts/mb-liqtest-create-group.ts b/ts/client/src/scripts/mb-liqtest-create-group.ts index 340e586d1..a2a8d58a3 100644 --- a/ts/client/src/scripts/mb-liqtest-create-group.ts +++ b/ts/client/src/scripts/mb-liqtest-create-group.ts @@ -1,8 +1,14 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; -import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { + AddressLookupTableProgram, + Connection, + Keypair, + PublicKey, +} from '@solana/web3.js'; import fs from 'fs'; import { MangoClient } from '../client'; import { MANGO_V4_ID } from '../constants'; +import { buildVersionedTx } from '../utils'; // // Script which depoys a new mango group, and registers 3 tokens @@ -14,23 +20,23 @@ const GROUP_NUM = Number(process.env.GROUP_NUM || 200); const MAINNET_MINTS = new Map([ ['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], - ['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], + ['ETH', '7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs'], ['SOL', 'So11111111111111111111111111111111111111112'], ['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'], ]); const STUB_PRICES = new Map([ ['USDC', 1.0], - ['BTC', 20000.0], // btc and usdc both have 6 decimals - ['SOL', 0.04], // sol has 9 decimals, equivalent to $40 per SOL - ['MNGO', 0.04], // same price/decimals as SOL for convenience + ['ETH', 1200.0], // eth and usdc both have 6 decimals + ['SOL', 0.015], // sol has 9 decimals, equivalent to $15 per SOL + ['MNGO', 0.02], // same price/decimals as SOL for convenience ]); // External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json // and verified to have best liquidity for pair on https://openserum.io/ const MAINNET_SERUM3_MARKETS = new Map([ - ['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'], - ['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'], + ['ETH/USDC', 'FZxi3yWkE5mMjyaZj6utmYL54QQYfMCKMcLaQZq4UwnA'], + ['SOL/USDC', '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'], ]); const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2; @@ -130,17 +136,17 @@ async function main() { } // register token 1 - console.log(`Registering BTC...`); - const btcMainnetMint = new PublicKey(MAINNET_MINTS.get('BTC')!); - const btcMainnetOracle = oracles.get('BTC'); + console.log(`Registering ETH...`); + const ethMainnetMint = new PublicKey(MAINNET_MINTS.get('ETH')!); + const ethMainnetOracle = oracles.get('ETH'); try { await client.tokenRegister( group, - btcMainnetMint, - btcMainnetOracle, + ethMainnetMint, + ethMainnetOracle, defaultOracleConfig, 1, - 'BTC', + 'ETH', defaultInterestRate, 0.0, 0.0001, @@ -215,7 +221,7 @@ async function main() { 0, 'MNGO-PERP', defaultOracleConfig, - 9, + 6, 10, 100000, // base lots 0.9, @@ -235,14 +241,134 @@ async function main() { 0, 0, 0, - 1.0, + -1.0, 2 * 60 * 60, ); } catch (error) { console.log(error); } + await createAndPopulateAlt(client, admin); + process.exit(); } main(); + +async function createAndPopulateAlt(client: MangoClient, admin: Keypair) { + let group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + + const connection = client.program.provider.connection; + + // Create ALT, and set to group at index 0 + if (group.addressLookupTables[0].equals(PublicKey.default)) { + try { + console.log(`ALT: Creating`); + const createIx = AddressLookupTableProgram.createLookupTable({ + authority: admin.publicKey, + payer: admin.publicKey, + recentSlot: await connection.getSlot('finalized'), + }); + const createTx = await buildVersionedTx( + client.program.provider as AnchorProvider, + [createIx[0]], + ); + let sig = await connection.sendTransaction(createTx); + console.log( + `...created ALT ${createIx[1]} https://explorer.solana.com/tx/${sig}`, + ); + + console.log(`ALT: set at index 0 for group...`); + sig = await client.altSet(group, createIx[1], 0); + console.log(`...https://explorer.solana.com/tx/${sig}`); + + group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + } catch (error) { + console.log(error); + } + } + + // Extend using mango v4 relevant pub keys + try { + let bankAddresses = Array.from(group.banksMapByMint.values()) + .flat() + .map((bank) => [bank.publicKey, bank.oracle, bank.vault]) + .flat() + .concat( + Array.from(group.banksMapByMint.values()) + .flat() + .map((mintInfo) => mintInfo.publicKey), + ); + + let serum3MarketAddresses = Array.from( + group.serum3MarketsMapByExternal.values(), + ) + .flat() + .map((serum3Market) => serum3Market.publicKey); + + let serum3ExternalMarketAddresses = Array.from( + group.serum3ExternalMarketsMap.values(), + ) + .flat() + .map((serum3ExternalMarket) => [ + serum3ExternalMarket.publicKey, + serum3ExternalMarket.bidsAddress, + serum3ExternalMarket.asksAddress, + ]) + .flat(); + + let perpMarketAddresses = Array.from( + group.perpMarketsMapByMarketIndex.values(), + ) + .flat() + .map((perpMarket) => [ + perpMarket.publicKey, + perpMarket.oracle, + perpMarket.bids, + perpMarket.asks, + perpMarket.eventQueue, + ]) + .flat(); + + async function extendTable(addresses: PublicKey[]) { + await group.reloadAll(client); + const alt = + await client.program.provider.connection.getAddressLookupTable( + group.addressLookupTables[0], + ); + + addresses = addresses.filter( + (newAddress) => + alt.value?.state.addresses && + alt.value?.state.addresses.findIndex((addressInALt) => + addressInALt.equals(newAddress), + ) === -1, + ); + if (addresses.length === 0) { + return; + } + const extendIx = AddressLookupTableProgram.extendLookupTable({ + lookupTable: group.addressLookupTables[0], + payer: admin.publicKey, + authority: admin.publicKey, + addresses, + }); + const extendTx = await buildVersionedTx( + client.program.provider as AnchorProvider, + [extendIx], + ); + let sig = await client.program.provider.connection.sendTransaction( + extendTx, + ); + console.log(`https://explorer.solana.com/tx/${sig}`); + } + + console.log(`ALT: extending using mango v4 relevant public keys`); + await extendTable(bankAddresses); + await extendTable(serum3MarketAddresses); + await extendTable(serum3ExternalMarketAddresses); + await extendTable(perpMarketAddresses); + } catch (error) { + console.log(error); + } +} diff --git a/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts b/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts deleted file mode 100644 index ab61da01a..000000000 --- a/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { AnchorProvider, Wallet } from '@project-serum/anchor'; -import { Connection, Keypair } from '@solana/web3.js'; -import fs from 'fs'; -import { MangoClient } from '../client'; -import { MANGO_V4_ID } from '../constants'; - -// -// This script deposits some tokens, so other liquidation scripts can borrow. -// - -const GROUP_NUM = Number(process.env.GROUP_NUM || 200); -const ACCOUNT_NUM = Number(process.env.ACCOUNT_NUM || 0); - -async function main() { - const options = AnchorProvider.defaultOptions(); - const connection = new Connection(process.env.CLUSTER_URL!, options); - - const admin = Keypair.fromSecretKey( - Buffer.from( - JSON.parse( - fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'), - ), - ), - ); - const userWallet = new Wallet(admin); - const userProvider = new AnchorProvider(connection, userWallet, options); - const client = await MangoClient.connect( - userProvider, - 'mainnet-beta', - MANGO_V4_ID['mainnet-beta'], - { - idsSource: 'get-program-accounts', - }, - ); - console.log(`User ${userWallet.publicKey.toBase58()}`); - - // fetch group - const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); - console.log(group.toString()); - - // create + fetch account - console.log(`Creating mangoaccount...`); - const mangoAccount = (await client.createAndFetchMangoAccount( - group, - ACCOUNT_NUM, - 'LIQTEST, FUNDING', - 8, - 4, - 4, - 4, - ))!; - console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); - console.log(mangoAccount.toString()); - - const usdcMint = group.banksMapByName.get('USDC')![0].mint; - const btcMint = group.banksMapByName.get('BTC')![0].mint; - const solMint = group.banksMapByName.get('SOL')![0].mint; - - // deposit - try { - console.log(`...depositing 5 USDC`); - await client.tokenDeposit(group, mangoAccount, usdcMint, 5); - await mangoAccount.reload(client); - - console.log(`...depositing 0.0002 BTC`); - await client.tokenDeposit(group, mangoAccount, btcMint, 0.0002); - await mangoAccount.reload(client); - - console.log(`...depositing 0.15 SOL`); - await client.tokenDeposit(group, mangoAccount, solMint, 0.15); - await mangoAccount.reload(client); - } catch (error) { - console.log(error); - } - - process.exit(); -} - -main(); diff --git a/ts/client/src/scripts/mb-liqtest-make-candidates.ts b/ts/client/src/scripts/mb-liqtest-make-candidates.ts index 9ee49630c..9f066e840 100644 --- a/ts/client/src/scripts/mb-liqtest-make-candidates.ts +++ b/ts/client/src/scripts/mb-liqtest-make-candidates.ts @@ -1,8 +1,9 @@ import { AnchorProvider, BN, Wallet } from '@project-serum/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; +import { Bank } from '../accounts/bank'; import { MangoAccount } from '../accounts/mangoAccount'; -import { PerpOrderSide, PerpOrderType } from '../accounts/perp'; +import { PerpMarket, PerpOrderSide, PerpOrderType } from '../accounts/perp'; import { Serum3OrderType, Serum3SelfTradeBehavior, @@ -19,24 +20,30 @@ const GROUP_NUM = Number(process.env.GROUP_NUM || 200); // native prices const PRICES = { - BTC: 20000.0, - SOL: 0.04, + ETH: 1200.0, + SOL: 0.015, USDC: 1, - MNGO: 0.04, + MNGO: 0.02, }; -const MAINNET_MINTS = new Map([ - ['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], - ['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], - ['SOL', 'So11111111111111111111111111111111111111112'], - ['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'], -]); - -const TOKEN_SCENARIOS: [string, string, number, string, number][] = [ - ['LIQTEST, LIQOR', 'USDC', 1000000, 'USDC', 0], - ['LIQTEST, A: USDC, L: SOL', 'USDC', 1000 * PRICES.SOL, 'SOL', 920], - ['LIQTEST, A: SOL, L: USDC', 'SOL', 1000, 'USDC', 920 * PRICES.SOL], - ['LIQTEST, A: BTC, L: SOL', 'BTC', 20, 'SOL', (18 * PRICES.BTC) / PRICES.SOL], +const TOKEN_SCENARIOS: [string, [string, number][], [string, number][]][] = [ + [ + 'LIQTEST, FUNDING', + [ + ['USDC', 5000000], + ['ETH', 100000], + ['SOL', 150000000], + ], + [], + ], + ['LIQTEST, LIQOR', [['USDC', 1000000]], []], + ['LIQTEST, A: USDC, L: SOL', [['USDC', 1000 * PRICES.SOL]], [['SOL', 920]]], + ['LIQTEST, A: SOL, L: USDC', [['SOL', 1000]], [['USDC', 990 * PRICES.SOL]]], + [ + 'LIQTEST, A: ETH, L: SOL', + [['ETH', 20]], + [['SOL', (18 * PRICES.ETH) / PRICES.SOL]], + ], ]; async function main() { @@ -66,17 +73,17 @@ async function main() { const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); console.log(group.toString()); + const MAINNET_MINTS = new Map([ + ['USDC', group.banksMapByName.get('USDC')![0].mint], + ['ETH', group.banksMapByName.get('ETH')![0].mint], + ['SOL', group.banksMapByName.get('SOL')![0].mint], + ]); + const accounts = await client.getMangoAccountsForOwner( group, admin.publicKey, ); let maxAccountNum = Math.max(0, ...accounts.map((a) => a.accountNum)); - const fundingAccount = accounts.find( - (account) => account.name == 'LIQTEST, FUNDING', - ); - if (!fundingAccount) { - throw new Error('could not find funding account'); - } async function createMangoAccount(name: string): Promise { const accountNum = maxAccountNum + 1; @@ -89,8 +96,73 @@ async function main() { ))!; } + async function setBankPrice(bank: Bank, price: number): Promise { + await client.stubOracleSet(group, bank.oracle, price); + // reset stable price + await client.tokenEdit( + group, + bank.mint, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + true, + null, + ); + } + async function setPerpPrice( + perpMarket: PerpMarket, + price: number, + ): Promise { + await client.stubOracleSet(group, perpMarket.oracle, price); + // reset stable price + await client.perpEditMarket( + group, + perpMarket.perpMarketIndex, + perpMarket.oracle, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + ); + } + for (const scenario of TOKEN_SCENARIOS) { - const [name, assetName, assetAmount, liabName, liabAmount] = scenario; + const [name, assets, liabs] = scenario; // create account console.log(`Creating mangoaccount...`); @@ -99,22 +171,24 @@ async function main() { `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); - const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!); - const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!); + for (let [assetName, assetAmount] of assets) { + const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!); + await client.tokenDepositNative( + group, + mangoAccount, + assetMint, + new BN(assetAmount), + ); + await mangoAccount.reload(client); + } - await client.tokenDepositNative( - group, - mangoAccount, - assetMint, - new BN(assetAmount), - ); - await mangoAccount.reload(client); + for (let [liabName, liabAmount] of liabs) { + const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!); - if (liabAmount > 0) { // temporarily drop the borrowed token value, so the borrow goes through - const oracle = group.banksMapByName.get(liabName)![0].oracle; + const bank = group.banksMapByName.get(liabName)![0]; try { - await client.stubOracleSet(group, oracle, PRICES[liabName] / 2); + await setBankPrice(bank, PRICES[liabName] / 2); await client.tokenWithdrawNative( group, @@ -125,11 +199,22 @@ async function main() { ); } finally { // restore the oracle - await client.stubOracleSet(group, oracle, PRICES[liabName]); + await setBankPrice(bank, PRICES[liabName]); } } } + const accounts2 = await client.getMangoAccountsForOwner( + group, + admin.publicKey, + ); + const fundingAccount = accounts2.find( + (account) => account.name == 'LIQTEST, FUNDING', + ); + if (!fundingAccount) { + throw new Error('could not find funding account'); + } + // Serum order scenario { const name = 'LIQTEST, serum orders'; @@ -233,19 +318,18 @@ async function main() { `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); - const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!); const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!); - const collateralOracle = group.banksMapByName.get('SOL')![0].oracle; + const collateralBank = group.banksMapByName.get('SOL')![0]; await client.tokenDepositNative( group, mangoAccount, collateralMint, - new BN(100000), - ); // valued as $0.004 maint collateral + new BN(300000), + ); // valued as 0.0003 SOL, $0.0045 maint collateral await mangoAccount.reload(client); - await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 4); + await setBankPrice(collateralBank, PRICES['SOL'] * 4); try { await client.perpPlaceOrder( @@ -253,9 +337,9 @@ async function main() { mangoAccount, group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, PerpOrderSide.bid, - 1, // ui price that won't get hit - 0.0011, // ui base quantity, 11 base lots, $0.044 - 0.044, // ui quote quantity + 0.001, // ui price that won't get hit + 1.1, // ui base quantity, 11 base lots, 1.1 MNGO, $0.022 + 0.022, // ui quote quantity 4200, PerpOrderType.limit, false, @@ -263,7 +347,7 @@ async function main() { 5, ); } finally { - await client.stubOracleSet(group, collateralOracle, PRICES['SOL']); + await setBankPrice(collateralBank, PRICES['SOL']); } } @@ -277,19 +361,18 @@ async function main() { `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); - const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!); const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!); - const collateralOracle = group.banksMapByName.get('SOL')![0].oracle; + const collateralBank = group.banksMapByName.get('SOL')![0]; await client.tokenDepositNative( group, mangoAccount, collateralMint, - new BN(100000), - ); // valued as $0.004 maint collateral + new BN(300000), + ); // valued as 0.0003 SOL, $0.0045 maint collateral await mangoAccount.reload(client); - await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 5); + await setBankPrice(collateralBank, PRICES['SOL'] * 10); try { await client.perpPlaceOrder( @@ -297,9 +380,9 @@ async function main() { fundingAccount, group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, PerpOrderSide.ask, - 40, - 0.0011, // ui base quantity, 11 base lots, $0.044 - 0.044, // ui quote quantity + 0.03, + 1.1, // ui base quantity, 11 base lots, $0.022 value, gain $0.033 + 0.033, // ui quote quantity 4200, PerpOrderType.limit, false, @@ -312,9 +395,9 @@ async function main() { mangoAccount, group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, PerpOrderSide.bid, - 40, - 0.0011, // ui base quantity, 11 base lots, $0.044 - 0.044, // ui quote quantity + 0.03, + 1.1, // ui base quantity, 11 base lots, $0.022 value, cost $0.033 + 0.033, // ui quote quantity 4200, PerpOrderType.market, false, @@ -327,7 +410,7 @@ async function main() { group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, ); } finally { - await client.stubOracleSet(group, collateralOracle, PRICES['SOL']); + await setBankPrice(collateralBank, PRICES['SOL']); } } @@ -341,23 +424,22 @@ async function main() { `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); - const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!); - const baseOracle = (await client.getStubOracle(group, baseMint))[0] - .publicKey; + const perpMarket = group.perpMarketsMapByName.get('MNGO-PERP')!; + const perpIndex = perpMarket.perpMarketIndex; const liabMint = new PublicKey(MAINNET_MINTS.get('USDC')!); const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!); - const collateralOracle = group.banksMapByName.get('SOL')![0].oracle; + const collateralBank = group.banksMapByName.get('SOL')![0]; await client.tokenDepositNative( group, mangoAccount, collateralMint, - new BN(100000), - ); // valued as $0.004 maint collateral + new BN(300000), + ); // valued as $0.0045 maint collateral await mangoAccount.reload(client); try { - await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 10); + await setBankPrice(collateralBank, PRICES['SOL'] * 10); // Spot-borrow more than the collateral is worth await client.tokenWithdrawNative( @@ -369,16 +451,16 @@ async function main() { ); await mangoAccount.reload(client); - // Execute two trades that leave the account with +$0.022 positive pnl - await client.stubOracleSet(group, baseOracle, PRICES['MNGO'] / 2); + // Execute two trades that leave the account with +$0.011 positive pnl + await setPerpPrice(perpMarket, PRICES['MNGO'] / 2); await client.perpPlaceOrder( group, fundingAccount, - group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, + perpIndex, PerpOrderSide.ask, - 20, - 0.0011, // ui base quantity, 11 base lots, $0.022 - 0.022, // ui quote quantity + 0.01, + 1.1, // ui base quantity, 11 base lots, $0.011 + 0.011, // ui quote quantity 4200, PerpOrderType.limit, false, @@ -388,32 +470,29 @@ async function main() { await client.perpPlaceOrder( group, mangoAccount, - group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, + perpIndex, PerpOrderSide.bid, - 20, - 0.0011, // ui base quantity, 11 base lots, $0.022 - 0.022, // ui quote quantity + 0.01, + 1.1, // ui base quantity, 11 base lots, $0.011 + 0.011, // ui quote quantity 4200, PerpOrderType.market, false, 0, 5, ); - await client.perpConsumeAllEvents( - group, - group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, - ); + await client.perpConsumeAllEvents(group, perpIndex); - await client.stubOracleSet(group, baseOracle, PRICES['MNGO']); + await setPerpPrice(perpMarket, PRICES['MNGO']); await client.perpPlaceOrder( group, fundingAccount, - group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, + perpIndex, PerpOrderSide.bid, - 40, - 0.0011, // ui base quantity, 11 base lots, $0.044 - 0.044, // ui quote quantity + 0.02, + 1.1, // ui base quantity, 11 base lots, $0.022 + 0.022, // ui quote quantity 4201, PerpOrderType.limit, false, @@ -423,24 +502,21 @@ async function main() { await client.perpPlaceOrder( group, mangoAccount, - group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, + perpIndex, PerpOrderSide.ask, - 40, - 0.0011, // ui base quantity, 11 base lots, $0.044 - 0.044, // ui quote quantity + 0.02, + 1.1, // ui base quantity, 11 base lots, $0.022 + 0.022, // ui quote quantity 4201, PerpOrderType.market, false, 0, 5, ); - await client.perpConsumeAllEvents( - group, - group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, - ); + await client.perpConsumeAllEvents(group, perpIndex); } finally { - await client.stubOracleSet(group, collateralOracle, PRICES['SOL']); - await client.stubOracleSet(group, baseOracle, PRICES['MNGO']); + await setPerpPrice(perpMarket, PRICES['MNGO']); + await setBankPrice(collateralBank, PRICES['SOL']); } }