From 0040a1477e55c7b5c112540ac9a96f23faa02d57 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 21 Sep 2022 11:45:05 +0200 Subject: [PATCH 01/39] Client: Compute max perp trade amount (#231) --- programs/mango-v4/src/state/health.rs | 362 ++++++++++++++++++++++---- 1 file changed, 317 insertions(+), 45 deletions(-) diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 7d10b882d..fdfac4acc 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -17,6 +17,9 @@ use crate::state::{ }; use crate::util::checked_math as cm; +#[cfg(feature = "client")] +use crate::state::orderbook::order_type::Side as PerpOrderSide; + use super::MangoAccountRef; const ONE_NATIVE_USDC_IN_USD: I80F48 = I80F48!(0.000001); @@ -521,8 +524,8 @@ pub struct PerpInfo { pub base: I80F48, // in health-reference-token native units, no asset/liab factor needed pub quote: I80F48, - oracle_price: I80F48, - has_open_orders: bool, + pub oracle_price: I80F48, + pub has_open_orders: bool, } impl PerpInfo { @@ -626,6 +629,15 @@ impl PerpInfo { /// balances they could now borrow other assets). #[inline(always)] fn health_contribution(&self, health_type: HealthType) -> I80F48 { + let c = self.uncapped_health_contribution(health_type); + + // FUTURE: Allow v3-style "reliable" markets where we can return + // `self.quote + weight * self.base` here + c.min(I80F48::ZERO) + } + + #[inline(always)] + fn uncapped_health_contribution(&self, health_type: HealthType) -> I80F48 { let weight = match (health_type, self.base.is_negative()) { (HealthType::Init, true) => self.init_liab_weight, (HealthType::Init, false) => self.init_asset_weight, @@ -633,9 +645,7 @@ impl PerpInfo { (HealthType::Maint, false) => self.maint_asset_weight, }; - // FUTURE: Allow v3-style "reliable" markets where we can return - // `self.quote + weight * self.base` here - cm!(self.quote + weight * self.base).min(I80F48::ZERO) + cm!(self.quote + weight * self.base) } } @@ -691,6 +701,14 @@ impl HealthCache { .ok_or_else(|| error_msg!("token index {} not found", token_index)) } + #[cfg(feature = "client")] + fn perp_info_index(&self, perp_market_index: PerpMarketIndex) -> Result { + self.perp_infos + .iter() + .position(|pi| pi.perp_market_index == perp_market_index) + .ok_or_else(|| error_msg!("perp market index {} not found", perp_market_index)) + } + pub fn adjust_token_balance(&mut self, token_index: TokenIndex, change: I80F48) -> Result<()> { let entry_index = self.token_info_index(token_index)?; let mut entry = &mut self.token_infos[entry_index]; @@ -942,40 +960,6 @@ impl HealthCache { ) }; - let binary_approximation_search = - |mut left, - left_ratio: I80F48, - mut right, - mut right_ratio: I80F48, - target_ratio: I80F48| { - let max_iterations = 20; - let target_error = I80F48!(0.01); - require_msg!( - (left_ratio - target_ratio).signum() * (right_ratio - target_ratio).signum() - != I80F48::ONE, - "internal error: left {} and right {} don't contain the target value {}", - left_ratio, - right_ratio, - target_ratio - ); - for _ in 0..max_iterations { - let new = I80F48::from_num(0.5) * (left + right); - let new_ratio = health_ratio_after_swap(new); - let error = new_ratio - target_ratio; - if error > 0 && error < target_error { - return Ok(new); - } - - if (new_ratio > target_ratio) ^ (right_ratio > target_ratio) { - left = new; - } else { - right = new; - right_ratio = new_ratio; - } - } - Err(error_msg!("binary search iterations exhausted")) - }; - let amount = if initial_ratio <= min_ratio && point0_ratio < min_ratio && point1_ratio < min_ratio { // If we have to stay below the target ratio, pick the highest one @@ -1000,35 +984,178 @@ impl HealthCache { } let zero_health_amount = point1_amount - point1_health / final_health_slope; let zero_health_ratio = health_ratio_after_swap(zero_health_amount); - binary_approximation_search( + binary_search( point1_amount, point1_ratio, zero_health_amount, zero_health_ratio, min_ratio, + health_ratio_after_swap, )? } else if point0_ratio >= min_ratio { // Must be between point0_amount and point1_amount. - binary_approximation_search( + binary_search( point0_amount, point0_ratio, point1_amount, point1_ratio, min_ratio, + health_ratio_after_swap, )? } else { // Must be between 0 and point0_amount - binary_approximation_search( + binary_search( I80F48::ZERO, initial_ratio, point0_amount, point0_ratio, min_ratio, + health_ratio_after_swap, )? }; Ok(amount / source.oracle_price) } + + #[cfg(feature = "client")] + pub fn max_perp_for_health_ratio( + &self, + perp_market_index: PerpMarketIndex, + price: I80F48, + base_lot_size: i64, + side: PerpOrderSide, + min_ratio: I80F48, + ) -> Result { + let base_lot_size = I80F48::from(base_lot_size); + + let initial_ratio = self.health_ratio(HealthType::Init); + if initial_ratio < 0 { + return Ok(0); + } + + let direction = match side { + PerpOrderSide::Bid => 1, + PerpOrderSide::Ask => -1, + }; + + let perp_info_index = self.perp_info_index(perp_market_index)?; + let perp_info = &self.perp_infos[perp_info_index]; + let oracle_price = perp_info.oracle_price; + + // If the price is sufficiently good then health will just increase from trading + let final_health_slope = if direction == 1 { + perp_info.init_asset_weight * oracle_price - price + } else { + price - perp_info.init_liab_weight * oracle_price + }; + if final_health_slope >= 0 { + return Ok(i64::MAX); + } + + let cache_after_trade = |base_lots: I80F48| { + let mut adjusted_cache = self.clone(); + let d = I80F48::from(direction); + adjusted_cache.perp_infos[perp_info_index].base += + d * base_lots * base_lot_size * oracle_price; + adjusted_cache.perp_infos[perp_info_index].quote -= + d * base_lots * base_lot_size * price; + adjusted_cache + }; + let health_ratio_after_trade = + |base_lots| cache_after_trade(base_lots).health_ratio(HealthType::Init); + + // This is awkward, can we pass the base_lots and lot_size in PerpInfo? + let initial_base_lots = perp_info.base / perp_info.oracle_price / base_lot_size; + + // There are two cases: + // 1. We are increasing abs(base_lots) + // 2. We are bringing the base position to 0, and then going to case 1. + let has_case2 = + initial_base_lots > 0 && direction == -1 || initial_base_lots < 0 && direction == 1; + + let (case1_start, case1_start_ratio) = if has_case2 { + let case1_start = initial_base_lots.abs(); + let case1_start_ratio = health_ratio_after_trade(case1_start); + (case1_start, case1_start_ratio) + } else { + (I80F48::ZERO, initial_ratio) + }; + + // If we start out below min_ratio and can't go above, pick the best case + let base_lots = if initial_ratio <= min_ratio && case1_start_ratio < min_ratio { + if case1_start_ratio >= initial_ratio { + case1_start + } else { + I80F48::ZERO + } + } else if case1_start_ratio >= min_ratio { + // Must reach min_ratio to the right of case1_start + let case1_start_health = cache_after_trade(case1_start).health(HealthType::Init); + if case1_start_health <= 0 { + return Ok(0); + } + let zero_health_amount = + case1_start - case1_start_health / final_health_slope / base_lot_size; + let zero_health_ratio = health_ratio_after_trade(zero_health_amount); + + binary_search( + case1_start, + case1_start_ratio, + zero_health_amount, + zero_health_ratio, + min_ratio, + health_ratio_after_trade, + )? + } else { + // Between 0 and case1_start + binary_search( + I80F48::ZERO, + initial_ratio, + case1_start, + case1_start_ratio, + min_ratio, + health_ratio_after_trade, + )? + }; + + // truncate result + Ok(base_lots.floor().to_num()) + } +} + +#[cfg(feature = "client")] +fn binary_search( + mut left: I80F48, + left_value: I80F48, + mut right: I80F48, + right_value: I80F48, + target_value: I80F48, + fun: impl Fn(I80F48) -> I80F48, +) -> Result { + let max_iterations = 20; + let target_error = I80F48!(0.01); + require_msg!( + (left_value - target_value).signum() * (right_value - target_value).signum() != I80F48::ONE, + "internal error: left {} and right {} don't contain the target value {}", + left_value, + right_value, + target_value + ); + for _ in 0..max_iterations { + let new = I80F48::from_num(0.5) * (left + right); + let new_value = fun(new); + let error = new_value - target_value; + if error > 0 && error < target_error { + return Ok(new); + } + + if (new_value > target_value) ^ (right_value > target_value) { + left = new; + } else { + right = new; + } + } + Err(error_msg!("binary search iterations exhausted")) } fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex) -> Result { @@ -1676,19 +1803,16 @@ mod tests { TokenInfo { token_index: 0, oracle_price: I80F48::from_num(2.0), - balance: I80F48::ZERO, ..default_token_info(0.1) }, TokenInfo { token_index: 1, oracle_price: I80F48::from_num(3.0), - balance: I80F48::ZERO, ..default_token_info(0.2) }, TokenInfo { token_index: 2, oracle_price: I80F48::from_num(4.0), - balance: I80F48::ZERO, ..default_token_info(0.3) }, ], @@ -1838,6 +1962,154 @@ mod tests { } } + #[test] + fn test_max_perp() { + let default_token_info = |x| TokenInfo { + token_index: 0, + maint_asset_weight: I80F48::from_num(1.0 - x), + init_asset_weight: I80F48::from_num(1.0 - x), + maint_liab_weight: I80F48::from_num(1.0 + x), + init_liab_weight: I80F48::from_num(1.0 + x), + oracle_price: I80F48::from_num(2.0), + balance: I80F48::ZERO, + serum3_max_reserved: I80F48::ZERO, + }; + let default_perp_info = |x| PerpInfo { + perp_market_index: 0, + maint_asset_weight: I80F48::from_num(1.0 - x), + init_asset_weight: I80F48::from_num(1.0 - x), + maint_liab_weight: I80F48::from_num(1.0 + x), + init_liab_weight: I80F48::from_num(1.0 + x), + base: I80F48::ZERO, + quote: I80F48::ZERO, + oracle_price: I80F48::from_num(2.0), + has_open_orders: false, + }; + + let health_cache = HealthCache { + token_infos: vec![TokenInfo { + token_index: 0, + oracle_price: I80F48::from_num(1.0), + balance: I80F48::ZERO, + ..default_token_info(0.0) + }], + serum3_infos: vec![], + perp_infos: vec![PerpInfo { + perp_market_index: 0, + ..default_perp_info(0.3) + }], + being_liquidated: false, + }; + let base_lot_size = 100; + + assert_eq!(health_cache.health(HealthType::Init), I80F48::ZERO); + assert_eq!(health_cache.health_ratio(HealthType::Init), I80F48::MAX); + assert_eq!( + health_cache + .max_perp_for_health_ratio( + 0, + I80F48::from(2), + base_lot_size, + PerpOrderSide::Bid, + I80F48::from_num(50.0) + ) + .unwrap(), + I80F48::ZERO + ); + + let adjust_token = |c: &mut HealthCache, value: f64| { + let ti = &mut c.token_infos[0]; + ti.balance += I80F48::from_num(value); + }; + let find_max_trade = + |c: &HealthCache, side: PerpOrderSide, ratio: f64, price_factor: f64| { + let oracle_price = c.perp_infos[0].oracle_price; + let trade_price = I80F48::from_num(price_factor) * oracle_price; + let base_lots = c + .max_perp_for_health_ratio( + 0, + trade_price, + base_lot_size, + side, + I80F48::from_num(ratio), + ) + .unwrap(); + if base_lots == i64::MAX { + return (i64::MAX, f64::MAX, f64::MAX); + } + + let direction = match side { + PerpOrderSide::Bid => 1, + PerpOrderSide::Ask => -1, + }; + + // compute the health ratio we'd get when executing the trade + let actual_ratio = { + let base_native = I80F48::from(direction * base_lots * base_lot_size); + let mut c = c.clone(); + c.perp_infos[0].base += base_native * oracle_price; + c.perp_infos[0].quote -= base_native * trade_price; + c.health_ratio(HealthType::Init).to_num::() + }; + // the ratio for trading just one base lot extra + let plus_ratio = { + let base_native = I80F48::from(direction * (base_lots + 1) * base_lot_size); + let mut c = c.clone(); + c.perp_infos[0].base += base_native * oracle_price; + c.perp_infos[0].quote -= base_native * trade_price; + c.health_ratio(HealthType::Init).to_num::() + }; + (base_lots, actual_ratio, plus_ratio) + }; + let check_max_trade = |c: &HealthCache, + side: PerpOrderSide, + ratio: f64, + price_factor: f64| { + let (base_lots, actual_ratio, plus_ratio) = + find_max_trade(c, side, ratio, price_factor); + println!( + "checking for price_factor: {price_factor}, target ratio {ratio}: actual ratio: {actual_ratio}, plus ratio: {plus_ratio}, base_lots: {base_lots}", + ); + let max_binary_search_error = 0.01; + assert!(ratio <= actual_ratio); + assert!(plus_ratio - max_binary_search_error <= ratio); + }; + + { + let mut health_cache = health_cache.clone(); + adjust_token(&mut health_cache, 3000.0); + + for existing in [-5, 0, 3] { + let mut c = health_cache.clone(); + c.perp_infos[0].base += I80F48::from(existing * base_lot_size * 2); + c.perp_infos[0].quote -= I80F48::from(existing * base_lot_size * 2); + + for side in [PerpOrderSide::Bid, PerpOrderSide::Ask] { + println!("test 0: existing {existing}, side {side:?}"); + for price_factor in [0.8, 1.0, 1.1] { + for ratio in 1..=100 { + check_max_trade(&health_cache, side, ratio as f64, price_factor); + } + } + } + } + + // check some extremely bad prices + check_max_trade(&health_cache, PerpOrderSide::Bid, 50.0, 2.0); + check_max_trade(&health_cache, PerpOrderSide::Ask, 50.0, 0.1); + + // and extremely good prices + assert_eq!( + find_max_trade(&health_cache, PerpOrderSide::Bid, 50.0, 0.1).0, + i64::MAX + ); + assert_eq!( + find_max_trade(&health_cache, PerpOrderSide::Ask, 50.0, 1.5).0, + i64::MAX + ); + } + } + #[test] fn test_health_perp_funding() { let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); From ae42356b222daaa6ecafbacf0e8194e849e2f215 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Thu, 22 Sep 2022 17:43:01 +0200 Subject: [PATCH 02/39] program: remove todos for ix which have now been implemented or not relevant (#240) Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- programs/mango-v4/src/lib.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 45f7d2746..a0d252e4c 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -268,8 +268,6 @@ pub mod mango_v4 { instructions::serum3_deregister_market(ctx) } - // TODO serum3_change_spot_market_params - pub fn serum3_create_open_orders(ctx: Context) -> Result<()> { instructions::serum3_create_open_orders(ctx) } @@ -326,8 +324,6 @@ pub mod mango_v4 { instructions::serum3_liq_force_cancel_orders(ctx, limit) } - // TODO serum3_cancel_all_spot_orders - // DEPRECATED: use token_liq_with_token pub fn liq_token_with_token( ctx: Context, @@ -464,8 +460,6 @@ pub mod mango_v4 { instructions::perp_close_market(ctx) } - // TODO perp_change_perp_market_params - pub fn perp_deactivate_position(ctx: Context) -> Result<()> { instructions::perp_deactivate_position(ctx) } From 55f77ad6c62d93484a8892a8952c7e5a4be0ba61 Mon Sep 17 00:00:00 2001 From: Conj0iner Date: Fri, 23 Sep 2022 00:55:12 +0800 Subject: [PATCH 03/39] Added IOC penalty --- .../src/instructions/perp_create_market.rs | 4 +- .../src/instructions/perp_edit_market.rs | 4 + programs/mango-v4/src/lib.rs | 4 + .../src/state/mango_account_components.rs | 70 +++--------- programs/mango-v4/src/state/orderbook/book.rs | 23 +++- programs/mango-v4/src/state/orderbook/mod.rs | 107 ++++++++++++++++++ programs/mango-v4/src/state/perp_market.rs | 67 ++++++++++- .../tests/program_test/mango_client.rs | 2 + 8 files changed, 218 insertions(+), 63 deletions(-) diff --git a/programs/mango-v4/src/instructions/perp_create_market.rs b/programs/mango-v4/src/instructions/perp_create_market.rs index e6a4410ba..7b28dfd77 100644 --- a/programs/mango-v4/src/instructions/perp_create_market.rs +++ b/programs/mango-v4/src/instructions/perp_create_market.rs @@ -64,6 +64,7 @@ pub fn perp_create_market( impact_quantity: i64, group_insurance_fund: bool, trusted_market: bool, + fee_penalty: f32, ) -> Result<()> { let mut perp_market = ctx.accounts.perp_market.load_init()?; *perp_market = PerpMarket { @@ -103,7 +104,8 @@ pub fn perp_create_market( padding0: Default::default(), padding1: Default::default(), padding2: Default::default(), - reserved: [0; 112], + fee_penalty, + reserved: [0; 108], }; let mut bids = ctx.accounts.bids.load_init()?; diff --git a/programs/mango-v4/src/instructions/perp_edit_market.rs b/programs/mango-v4/src/instructions/perp_edit_market.rs index 026ac6f9f..3508f603d 100644 --- a/programs/mango-v4/src/instructions/perp_edit_market.rs +++ b/programs/mango-v4/src/instructions/perp_edit_market.rs @@ -35,6 +35,7 @@ pub fn perp_edit_market( impact_quantity_opt: Option, group_insurance_fund_opt: Option, trusted_market_opt: Option, + fee_penalty_opt: Option, ) -> Result<()> { let mut perp_market = ctx.accounts.perp_market.load_mut()?; @@ -91,6 +92,9 @@ pub fn perp_edit_market( if let Some(impact_quantity) = impact_quantity_opt { perp_market.impact_quantity = impact_quantity; } + if let Some(fee_penalty) = fee_penalty_opt { + perp_market.fee_penalty = fee_penalty; + } // unchanged - // long_funding diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index a0d252e4c..69423a08f 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -393,6 +393,7 @@ pub mod mango_v4 { impact_quantity: i64, group_insurance_fund: bool, trusted_market: bool, + fee_penalty: f32, ) -> Result<()> { instructions::perp_create_market( ctx, @@ -414,6 +415,7 @@ pub mod mango_v4 { impact_quantity, group_insurance_fund, trusted_market, + fee_penalty, ) } @@ -435,6 +437,7 @@ pub mod mango_v4 { impact_quantity_opt: Option, group_insurance_fund_opt: Option, trusted_market_opt: Option, + fee_penalty_opt: Option, ) -> Result<()> { instructions::perp_edit_market( ctx, @@ -453,6 +456,7 @@ pub mod mango_v4 { impact_quantity_opt, group_insurance_fund_opt, trusted_market_opt, + fee_penalty_opt, ) } diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index d7cb06c68..32c4924d1 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -400,8 +400,7 @@ pub use account_seeds; #[cfg(test)] mod tests { - use crate::state::{OracleConfig, PerpMarket}; - use anchor_lang::prelude::Pubkey; + use crate::state::PerpMarket; use fixed::types::I80F48; use rand::Rng; @@ -416,52 +415,9 @@ mod tests { pos } - fn create_perp_market() -> PerpMarket { - return PerpMarket { - group: Pubkey::new_unique(), - perp_market_index: 0, - group_insurance_fund: 0, - trusted_market: 0, - name: Default::default(), - oracle: Pubkey::new_unique(), - oracle_config: OracleConfig { - conf_filter: I80F48::ZERO, - }, - bids: Pubkey::new_unique(), - asks: Pubkey::new_unique(), - event_queue: Pubkey::new_unique(), - quote_lot_size: 1, - base_lot_size: 1, - maint_asset_weight: I80F48::from(1), - init_asset_weight: I80F48::from(1), - maint_liab_weight: I80F48::from(1), - init_liab_weight: I80F48::from(1), - liquidation_fee: I80F48::ZERO, - maker_fee: I80F48::ZERO, - taker_fee: I80F48::ZERO, - min_funding: I80F48::ZERO, - max_funding: I80F48::ZERO, - impact_quantity: 0, - long_funding: I80F48::ZERO, - short_funding: I80F48::ZERO, - funding_last_updated: 0, - open_interest: 0, - seq_num: 0, - fees_accrued: I80F48::ZERO, - fees_settled: I80F48::ZERO, - bump: 0, - base_decimals: 0, - reserved: [0; 112], - padding0: Default::default(), - padding1: Default::default(), - padding2: Default::default(), - registration_time: 0, - }; - } - #[test] fn test_quote_entry_long_increasing_from_zero() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(0, 0, 0); // Go long 10 @ 10 pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-100)); @@ -472,7 +428,7 @@ mod tests { #[test] fn test_quote_entry_short_increasing_from_zero() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(0, 0, 0); // Go short 10 @ 10 pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(100)); @@ -483,7 +439,7 @@ mod tests { #[test] fn test_quote_entry_long_increasing_from_long() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(10, -100, -100); // Go long 10 @ 30 pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-300)); @@ -494,7 +450,7 @@ mod tests { #[test] fn test_quote_entry_short_increasing_from_short() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(-10, 100, 100); // Go short 10 @ 10 pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(300)); @@ -505,7 +461,7 @@ mod tests { #[test] fn test_quote_entry_long_decreasing_from_short() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(-10, 100, 100); // Go long 5 @ 50 pos.change_base_and_quote_positions(&mut market, 5, I80F48::from(-250)); @@ -516,7 +472,7 @@ mod tests { #[test] fn test_quote_entry_short_decreasing_from_long() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(10, -100, -100); // Go short 5 @ 50 pos.change_base_and_quote_positions(&mut market, -5, I80F48::from(250)); @@ -527,7 +483,7 @@ mod tests { #[test] fn test_quote_entry_long_close_with_short() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(10, -100, -100); // Go short 10 @ 50 pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(250)); @@ -538,7 +494,7 @@ mod tests { #[test] fn test_quote_entry_short_close_with_long() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(-10, 100, 100); // Go long 10 @ 50 pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-250)); @@ -549,7 +505,7 @@ mod tests { #[test] fn test_quote_entry_long_close_short_with_overflow() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(10, -100, -100); // Go short 15 @ 20 pos.change_base_and_quote_positions(&mut market, -15, I80F48::from(300)); @@ -560,7 +516,7 @@ mod tests { #[test] fn test_quote_entry_short_close_long_with_overflow() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(-10, 100, 100); // Go short 15 @ 20 pos.change_base_and_quote_positions(&mut market, 15, I80F48::from(-300)); @@ -571,7 +527,7 @@ mod tests { #[test] fn test_quote_entry_break_even_price() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(0, 0, 0); // Buy 11 @ 10,000 pos.change_base_and_quote_positions(&mut market, 11, I80F48::from(-11 * 10_000)); @@ -585,7 +541,7 @@ mod tests { #[test] fn test_quote_entry_multiple_and_reversed_changes_return_entry_to_zero() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(0, 0, 0); // Generate array of random trades diff --git a/programs/mango-v4/src/state/orderbook/book.rs b/programs/mango-v4/src/state/orderbook/book.rs index dd96b4816..9f05f997b 100644 --- a/programs/mango-v4/src/state/orderbook/book.rs +++ b/programs/mango-v4/src/state/orderbook/book.rs @@ -377,6 +377,11 @@ impl<'a> Book<'a> { apply_fees(market, mango_account, total_quote_lots_taken)?; } + // IOC orders have a fee penalty applied regardless of match + if order_type == OrderType::ImmediateOrCancel { + apply_penalty(market, mango_account)?; + } + Ok(()) } @@ -460,12 +465,24 @@ fn apply_fees( // risks that fees_accrued is settled to 0 before they apply. It going negative // breaks assumptions. // The maker fees apply to the maker's account only when the fill event is consumed. - let maker_fees = taker_quote_native * market.maker_fee; + let maker_fees = cm!(taker_quote_native * market.maker_fee); + + let taker_fees = cm!(taker_quote_native * market.taker_fee); - let taker_fees = taker_quote_native * market.taker_fee; let perp_account = mango_account.perp_position_mut(market.perp_market_index)?; perp_account.change_quote_position(-taker_fees); - market.fees_accrued += taker_fees + maker_fees; + cm!(market.fees_accrued += taker_fees + maker_fees); + + Ok(()) +} + +/// Applies a fixed penalty fee to the account, and update the market's fees_accrued +fn apply_penalty(market: &mut PerpMarket, mango_account: &mut MangoAccountRefMut) -> Result<()> { + let perp_account = mango_account.perp_position_mut(market.perp_market_index)?; + let fee_penalty = I80F48::from_num(market.fee_penalty); + + perp_account.change_quote_position(-fee_penalty); + cm!(market.fees_accrued += fee_penalty); Ok(()) } diff --git a/programs/mango-v4/src/state/orderbook/mod.rs b/programs/mango-v4/src/state/orderbook/mod.rs index 4fc8ab3fe..9f4161df4 100644 --- a/programs/mango-v4/src/state/orderbook/mod.rs +++ b/programs/mango-v4/src/state/orderbook/mod.rs @@ -375,4 +375,111 @@ mod tests { match_quote - match_quote * market.taker_fee ); } + + #[test] + fn test_fee_penalty_applied_only_on_limit_order() -> Result<()> { + let (mut market, oracle_price, mut event_queue, bids, asks) = test_setup(1000.0); + let mut book = Book { + bids: bids.borrow_mut(), + asks: asks.borrow_mut(), + }; + + let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); + let taker_pk = Pubkey::new_unique(); + let now_ts = 1000000; + + market.base_lot_size = 1; + market.quote_lot_size = 1; + market.taker_fee = I80F48::from_num(0.01); + market.fee_penalty = 5.0; + account.ensure_perp_position(market.perp_market_index, 0)?; + + // Passive order + book.new_order( + Side::Ask, + &mut market, + &mut event_queue, + oracle_price, + &mut account.borrow_mut(), + &taker_pk, + 1000, + 2, + i64::MAX, + OrderType::Limit, + 0, + 43, + now_ts, + u8::MAX, + ) + .unwrap(); + + // Partial taker + book.new_order( + Side::Bid, + &mut market, + &mut event_queue, + oracle_price, + &mut account.borrow_mut(), + &taker_pk, + 1000, + 1, + i64::MAX, + OrderType::Limit, + 0, + 43, + now_ts, + u8::MAX, + ) + .unwrap(); + + let pos = account.perp_position(market.perp_market_index)?; + + assert_eq!( + pos.quote_position_native().round(), + I80F48::from_num(-10), + "Regular fees applied on limit order" + ); + + assert_eq!( + market.fees_accrued.round(), + I80F48::from_num(10), + "Fees moved to market" + ); + + // Full taker + book.new_order( + Side::Bid, + &mut market, + &mut event_queue, + oracle_price, + &mut account.borrow_mut(), + &taker_pk, + 1000, + 1, + i64::MAX, + OrderType::ImmediateOrCancel, + 0, + 43, + now_ts, + u8::MAX, + ) + .unwrap(); + + let pos = account.perp_position(market.perp_market_index)?; + + assert_eq!( + pos.quote_position_native().round(), + I80F48::from_num(-25), // -10 - 5 + "Regular fees + fixed penalty applied on IOC order" + ); + + assert_eq!( + market.fees_accrued.round(), + I80F48::from_num(25), // 10 + 5 + "Fees moved to market" + ); + + Ok(()) + } } diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 49a8d2e31..91a621181 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -99,12 +99,30 @@ pub struct PerpMarket { /// Fees settled in native quote currency pub fees_settled: I80F48, - pub reserved: [u8; 112], + pub fee_penalty: f32, + + pub reserved: [u8; 108], } const_assert_eq!( size_of::(), - 32 + 2 + 2 + 4 + 16 + 32 + 16 + 32 * 3 + 8 * 2 + 16 * 12 + 8 * 2 + 8 * 2 + 16 + 2 + 6 + 8 + 112 + 32 + 2 + + 2 + + 4 + + 16 + + 32 + + 16 + + 32 * 3 + + 8 * 2 + + 16 * 12 + + 8 * 2 + + 8 * 2 + + 16 + + 2 + + 6 + + 8 + + 4 + + 108 ); const_assert_eq!(size_of::() % 8, 0); @@ -225,4 +243,49 @@ impl PerpMarket { self.short_funding += socialized_loss; Ok(socialized_loss) } + + /// Creates default market for tests + pub fn default_for_tests() -> PerpMarket { + PerpMarket { + group: Pubkey::new_unique(), + perp_market_index: 0, + name: Default::default(), + oracle: Pubkey::new_unique(), + oracle_config: OracleConfig { + conf_filter: I80F48::ZERO, + }, + bids: Pubkey::new_unique(), + asks: Pubkey::new_unique(), + event_queue: Pubkey::new_unique(), + quote_lot_size: 1, + base_lot_size: 1, + maint_asset_weight: I80F48::from(1), + init_asset_weight: I80F48::from(1), + maint_liab_weight: I80F48::from(1), + init_liab_weight: I80F48::from(1), + liquidation_fee: I80F48::ZERO, + maker_fee: I80F48::ZERO, + taker_fee: I80F48::ZERO, + min_funding: I80F48::ZERO, + max_funding: I80F48::ZERO, + impact_quantity: 0, + long_funding: I80F48::ZERO, + short_funding: I80F48::ZERO, + funding_last_updated: 0, + open_interest: 0, + seq_num: 0, + fees_accrued: I80F48::ZERO, + fees_settled: I80F48::ZERO, + bump: 0, + base_decimals: 0, + reserved: [0; 108], + padding0: Default::default(), + padding1: Default::default(), + padding2: Default::default(), + registration_time: 0, + fee_penalty: 0.0, + trusted_market: 0, + group_insurance_fund: 0, + } + } } diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 49c634e2a..02c148597 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -2143,6 +2143,7 @@ pub struct PerpCreateMarketInstruction { pub taker_fee: f32, pub group_insurance_fund: bool, pub trusted_market: bool, + pub fee_penalty: f32, } impl PerpCreateMarketInstruction { pub async fn with_new_book_and_queue( @@ -2195,6 +2196,7 @@ impl ClientInstruction for PerpCreateMarketInstruction { base_decimals: self.base_decimals, group_insurance_fund: self.group_insurance_fund, trusted_market: self.trusted_market, + fee_penalty: self.fee_penalty, }; let perp_market = Pubkey::find_program_address( From a8ae59f0fe94a92babd373404aee7ee1b37a83e5 Mon Sep 17 00:00:00 2001 From: Conj0iner Date: Fri, 23 Sep 2022 01:03:45 +0800 Subject: [PATCH 04/39] IDL and client changes --- ts/client/src/client.ts | 4 ++++ ts/client/src/mango_v4.ts | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index c81847637..cef2c7521 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1322,6 +1322,7 @@ export class MangoClient { liquidationFee: number, makerFee: number, takerFee: number, + feePenalty: number, minFunding: number, maxFunding: number, impactQuantity: number, @@ -1356,6 +1357,7 @@ export class MangoClient { new BN(impactQuantity), groupInsuranceFund, trustedMarket, + feePenalty ) .accounts({ group: group.publicKey, @@ -1422,6 +1424,7 @@ export class MangoClient { liquidationFee: number, makerFee: number, takerFee: number, + feePenalty: number, minFunding: number, maxFunding: number, impactQuantity: number, @@ -1451,6 +1454,7 @@ export class MangoClient { new BN(impactQuantity), groupInsuranceFund, trustedMarket, + feePenalty ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 4d1c4ab1d..432432a40 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -2315,6 +2315,10 @@ export type MangoV4 = { { "name": "trustedMarket", "type": "bool" + }, + { + "name": "feePenalty", + "type": "f32" } ] }, @@ -2429,6 +2433,12 @@ export type MangoV4 = { "type": { "option": "bool" } + }, + { + "name": "feePenaltyOpt", + "type": { + "option": "f32" + } } ] }, @@ -4014,12 +4024,16 @@ export type MangoV4 = { "defined": "I80F48" } }, + { + "name": "feePenalty", + "type": "f32" + }, { "name": "reserved", "type": { "array": [ "u8", - 112 + 108 ] } } @@ -8412,6 +8426,10 @@ export const IDL: MangoV4 = { { "name": "trustedMarket", "type": "bool" + }, + { + "name": "feePenalty", + "type": "f32" } ] }, @@ -8526,6 +8544,12 @@ export const IDL: MangoV4 = { "type": { "option": "bool" } + }, + { + "name": "feePenaltyOpt", + "type": { + "option": "f32" + } } ] }, @@ -10111,12 +10135,16 @@ export const IDL: MangoV4 = { "defined": "I80F48" } }, + { + "name": "feePenalty", + "type": "f32" + }, { "name": "reserved", "type": { "array": [ "u8", - 112 + 108 ] } } From 3c98a9fd0acde384902288acf00a96b95bcd6881 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Fri, 23 Sep 2022 08:23:37 +0200 Subject: [PATCH 05/39] keeper: Fix logging of how many events are consumed (#235) * keeper: Fix logging of how many events are consumed Signed-off-by: microwavedcola1 * format Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- keeper/src/crank.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/keeper/src/crank.rs b/keeper/src/crank.rs index 0fdf8c56a..9279cf5f7 100644 --- a/keeper/src/crank.rs +++ b/keeper/src/crank.rs @@ -164,6 +164,7 @@ pub async fn loop_consume_events( client.program().account(perp_market.event_queue).unwrap(); let mut ams_ = vec![]; + let mut num_of_events = 0; // 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 @@ -197,6 +198,11 @@ pub async fn loop_consume_events( EventType::Liquidate => {} } event_queue.pop_front()?; + num_of_events+=1; + } + + if num_of_events == 0 { + return Ok(()); } let pre = Instant::now(); @@ -229,7 +235,7 @@ pub async fn loop_consume_events( "metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}", perp_market.name(), pre.elapsed().as_millis(), - ams_.len(), + num_of_events, e.to_string() ); log::error!("{:?}", e) @@ -238,7 +244,7 @@ pub async fn loop_consume_events( "metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}", perp_market.name(), pre.elapsed().as_millis(), - ams_.len(), + num_of_events, ); log::info!("{:?}", sig_result); } From ecbd4028f7a410e5518ea7f6b3a7d22f4f925182 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 21 Sep 2022 12:50:10 +0200 Subject: [PATCH 06/39] ts client: Fix remaining accounts when opening a perp position --- ts/client/src/client.ts | 90 +++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 57 deletions(-) diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index c81847637..289464ed3 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -31,6 +31,7 @@ import { MangoAccount, MangoAccountData, TokenPosition, + PerpPosition, } from './accounts/mangoAccount'; import { StubOracle } from './accounts/oracle'; import { PerpMarket, PerpOrderType, Side } from './accounts/perp'; @@ -1518,7 +1519,8 @@ export class MangoClient { AccountRetriever.Fixed, group, [mangoAccount], - [], + // Settlement token bank, because a position for it may be created + [group.getFirstBankByTokenIndex(0)], [perpMarket], ); return await this.program.methods @@ -1930,40 +1932,29 @@ export class MangoClient { ...mintInfos.map((mintInfo) => mintInfo.oracle), ); - healthRemainingAccounts.push( - ...mangoAccount.perps - .filter((perp) => perp.marketIndex !== 65535) - .map( - (perp) => - Array.from(group.perpMarketsMap.values()).filter( - (perpMarket) => perpMarket.perpMarketIndex === perp.marketIndex, - )[0].publicKey, - ), - ); + const allPerpIndices = mangoAccount.perps.map((perp) => perp.marketIndex); - healthRemainingAccounts.push( - ...mangoAccount.perps - .filter((perp) => perp.marketIndex !== 65535) - .map( - (perp) => - Array.from(group.perpMarketsMap.values()).filter( - (perpMarket) => perpMarket.perpMarketIndex === perp.marketIndex, - )[0].oracle, - ), - ); - - for (const perpMarket of perpMarkets) { - const alreadyAdded = mangoAccount.perps.find( - (p) => p.marketIndex === perpMarket.perpMarketIndex, - ); - if (!alreadyAdded) { - healthRemainingAccounts.push( - Array.from(group.perpMarketsMap.values()).filter( - (p) => p.perpMarketIndex === perpMarket.perpMarketIndex, - )[0].publicKey, - ); + // insert any extra perp markets in the free perp position slots + if (perpMarkets) { + for (const perpMarket of perpMarkets) { + if (allPerpIndices.indexOf(perpMarket.perpMarketIndex) < 0) { + allPerpIndices[ + mangoAccount.perps.findIndex( + (perp, index) => + !perp.isActive() && + allPerpIndices[index] == PerpPosition.PerpMarketIndexUnset, + ) + ] = perpMarket.perpMarketIndex; + } } } + const allPerpMarkets = allPerpIndices + .filter((index) => index != PerpPosition.PerpMarketIndexUnset) + .map((index) => group.findPerpMarket(index)!); + healthRemainingAccounts.push( + ...allPerpMarkets.map((perp) => perp.publicKey), + ); + healthRemainingAccounts.push(...allPerpMarkets.map((perp) => perp.oracle)); healthRemainingAccounts.push( ...mangoAccount.serum3 @@ -2010,39 +2001,24 @@ export class MangoClient { ...mintInfos.map((mintInfo) => mintInfo.oracle), ); - const perpsToAdd: PerpMarket[] = []; - + let perpIndices: number[] = []; for (const mangoAccount of mangoAccounts) { - perpsToAdd.push( + perpIndices.push( ...mangoAccount.perps .filter((perp) => perp.marketIndex !== 65535) - .map( - (perp) => - Array.from(group.perpMarketsMap.values()).filter( - (perpMarket) => perpMarket.perpMarketIndex === perp.marketIndex, - )[0], - ), + .map((perp) => perp.marketIndex), ); } - for (const mangoAccount of mangoAccounts) { - for (const perpMarket of perpMarkets) { - const alreadyAdded = mangoAccount.perps.find( - (p) => p.marketIndex === perpMarket.perpMarketIndex, - ); - if (!alreadyAdded) { - perpsToAdd.push( - Array.from(group.perpMarketsMap.values()).filter( - (p) => p.perpMarketIndex === perpMarket.perpMarketIndex, - )[0], - ); - } - } - } + perpIndices.push(...perpMarkets.map((perp) => perp.perpMarketIndex)); + + const allPerpMarkets = [...new Set(perpIndices)].map( + (marketIndex) => group.findPerpMarket(marketIndex)!, + ); // Add perp accounts - healthRemainingAccounts.push(...perpsToAdd.map((p) => p.publicKey)); + healthRemainingAccounts.push(...allPerpMarkets.map((p) => p.publicKey)); // Add oracle for each perp - healthRemainingAccounts.push(...perpsToAdd.map((p) => p.oracle)); + healthRemainingAccounts.push(...allPerpMarkets.map((p) => p.oracle)); for (const mangoAccount of mangoAccounts) { healthRemainingAccounts.push( From d86b3dd757a8660a30a79bf3e6ab907a11d5fcd6 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 21 Sep 2022 12:04:54 +0200 Subject: [PATCH 07/39] Rust client: Fix remaining accounts perp oracles --- client/src/client.rs | 5 +++++ client/src/context.rs | 34 ++++++++++++++++++---------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/client/src/client.rs b/client/src/client.rs index 96000770f..6f9f56b15 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -341,6 +341,10 @@ impl MangoClient { .active_perp_positions() .chain(account.active_perp_positions()) .map(|&pa| self.context.perp_market_address(pa.market_index)); + let perp_oracles = liqee + .active_perp_positions() + .chain(account.active_perp_positions()) + .map(|&pa| self.context.perp(pa.market_index).market.oracle); Ok(banks .iter() @@ -351,6 +355,7 @@ impl MangoClient { }) .chain(oracles.into_iter().map(to_readonly_account_meta)) .chain(perp_markets.map(to_readonly_account_meta)) + .chain(perp_oracles.map(to_readonly_account_meta)) .chain(serum_oos.map(to_readonly_account_meta)) .collect()) } diff --git a/client/src/context.rs b/client/src/context.rs index 6aa12fbf4..c83af29d5 100644 --- a/client/src/context.rs +++ b/client/src/context.rs @@ -78,6 +78,10 @@ impl MangoGroupContext { self.tokens.get(&token_index).unwrap() } + pub fn perp(&self, perp_market_index: PerpMarketIndex) -> &PerpMarketContext { + self.perp_markets.get(&perp_market_index).unwrap() + } + pub fn token_by_mint(&self, mint: &Pubkey) -> anyhow::Result<&TokenContext> { self.tokens .iter() @@ -86,7 +90,7 @@ impl MangoGroupContext { } pub fn perp_market_address(&self, perp_market_index: PerpMarketIndex) -> Pubkey { - self.perp_markets.get(&perp_market_index).unwrap().address + self.perp(perp_market_index).address } pub fn new_from_rpc( @@ -237,6 +241,15 @@ impl MangoGroupContext { let perp_markets = account .active_perp_positions() .map(|&pa| self.perp_market_address(pa.market_index)); + let perp_oracles = account + .active_perp_positions() + .map(|&pa| self.perp(pa.market_index).market.oracle); + + let to_account_meta = |pubkey| AccountMeta { + pubkey, + is_writable: false, + is_signer: false, + }; Ok(banks .iter() @@ -245,21 +258,10 @@ impl MangoGroupContext { is_writable: writable_banks, is_signer: false, }) - .chain(oracles.iter().map(|&pubkey| AccountMeta { - pubkey, - is_writable: false, - is_signer: false, - })) - .chain(perp_markets.map(|pubkey| AccountMeta { - pubkey, - is_writable: false, - is_signer: false, - })) - .chain(serum_oos.map(|pubkey| AccountMeta { - pubkey, - is_writable: false, - is_signer: false, - })) + .chain(oracles.into_iter().map(to_account_meta)) + .chain(perp_markets.map(to_account_meta)) + .chain(perp_oracles.map(to_account_meta)) + .chain(serum_oos.map(to_account_meta)) .collect()) } } From 4090cf407ea4949cae52651ff42b31bdeb36d121 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Fri, 23 Sep 2022 09:34:08 +0200 Subject: [PATCH 08/39] ts: fix max serum bid and ask that can be placed by a mango account (#241) * ts: fix getMaxQuoteForSerum3BidUi and getMaxBaseForSerum3AskUi where the zero amount was not tight enough for binary search Signed-off-by: microwavedcola1 * Fixes from review Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- ts/client/src/accounts/healthCache.ts | 109 +++++++++++++------ ts/client/src/accounts/mangoAccount.ts | 4 +- ts/client/src/debug-scripts/mb-debug-user.ts | 26 ++++- 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 8139d9b65..1ffc57a9b 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -177,18 +177,52 @@ export class HealthCache { return this.findTokenInfoIndex(bank.tokenIndex); } + findSerum3InfoIndex(marketIndex: number): number { + return this.serum3Infos.findIndex( + (serum3Info) => serum3Info.marketIndex === marketIndex, + ); + } + + getOrCreateSerum3InfoIndex(group: Group, serum3Market: Serum3Market): number { + const index = this.findSerum3InfoIndex(serum3Market.marketIndex); + const baseBank = group.getFirstBankByTokenIndex( + serum3Market.baseTokenIndex, + ); + const quoteBank = group.getFirstBankByTokenIndex( + serum3Market.quoteTokenIndex, + ); + const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank); + const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank); + if (index == -1) { + this.serum3Infos.push( + Serum3Info.emptyFromSerum3Market( + serum3Market, + baseEntryIndex, + quoteEntryIndex, + ), + ); + } + return this.findSerum3InfoIndex(serum3Market.marketIndex); + } + adjustSerum3Reserved( // todo change indices to types from numbers - marketIndex: number, - baseTokenIndex: number, + group: Group, + serum3Market: Serum3Market, reservedBaseChange: I80F48, freeBaseChange: I80F48, - quoteTokenIndex: number, reservedQuoteChange: I80F48, freeQuoteChange: I80F48, ) { - const baseEntryIndex = this.findTokenInfoIndex(baseTokenIndex); - const quoteEntryIndex = this.findTokenInfoIndex(quoteTokenIndex); + const baseBank = group.getFirstBankByTokenIndex( + serum3Market.baseTokenIndex, + ); + const quoteBank = group.getFirstBankByTokenIndex( + serum3Market.quoteTokenIndex, + ); + + const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank); + const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank); const baseEntry = this.tokenInfos[baseEntryIndex]; const reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice); @@ -203,14 +237,8 @@ export class HealthCache { quoteEntry.balance.iadd(freeQuoteChange.mul(quoteEntry.oraclePrice)); // Apply it to the serum3 info - const serum3Info = this.serum3Infos.find( - (serum3Info) => serum3Info.marketIndex === marketIndex, - ); - if (!serum3Info) { - throw new Error( - `Serum3Info not found for market with index ${marketIndex}`, - ); - } + const index = this.getOrCreateSerum3InfoIndex(group, serum3Market); + const serum3Info = this.serum3Infos[index]; serum3Info.reserved = serum3Info.reserved.add(reservedAmount); } @@ -288,11 +316,10 @@ export class HealthCache { // Increase reserved in Serum3Info for quote adjustedCache.adjustSerum3Reserved( - serum3Market.marketIndex, - serum3Market.baseTokenIndex, + group, + serum3Market, ZERO_I80F48(), ZERO_I80F48(), - serum3Market.quoteTokenIndex, bidNativeQuoteAmount, ZERO_I80F48(), ); @@ -325,11 +352,10 @@ export class HealthCache { // Increase reserved in Serum3Info for base adjustedCache.adjustSerum3Reserved( - serum3Market.marketIndex, - serum3Market.baseTokenIndex, + group, + serum3Market, askNativeBaseAmount, ZERO_I80F48(), - serum3Market.quoteTokenIndex, ZERO_I80F48(), ZERO_I80F48(), ); @@ -558,7 +584,8 @@ export class HealthCache { } // Amount which would bring health to 0 - // amount = max(A_deposits, B_borrows) + init_health / (A_liab_weight - B_asset_weight) + // where M = max(A_deposits, B_borrows) + // amount = M + (init_health + M * (B_init_liab - A_init_asset)) / (A_init_liab - B_init_asset); // A is what we would be essentially swapping for B // So when its an ask, then base->quote, // and when its a bid, then quote->bid @@ -567,30 +594,34 @@ export class HealthCache { const quoteBorrows = quote.balance.lt(ZERO_I80F48()) ? quote.balance.abs() : ZERO_I80F48(); - zeroAmount = base.balance - .max(quoteBorrows) - .add( - initialHealth.div( + const max = base.balance.max(quoteBorrows); + zeroAmount = max.add( + initialHealth + .add(max.mul(quote.initLiabWeight.sub(base.initAssetWeight))) + .div( base .liabWeight(HealthType.init) .sub(quote.assetWeight(HealthType.init)), ), - ); + ); } else { const baseBorrows = base.balance.lt(ZERO_I80F48()) ? base.balance.abs() : ZERO_I80F48(); - zeroAmount = quote.balance - .max(baseBorrows) - .add( - initialHealth.div( + const max = quote.balance.max(baseBorrows); + zeroAmount = max.add( + initialHealth + .add(max.mul(base.initLiabWeight.sub(quote.initAssetWeight))) + .div( quote .liabWeight(HealthType.init) .sub(base.assetWeight(HealthType.init)), ), - ); + ); } + const cache = cacheAfterPlacingOrder(zeroAmount); + const zeroAmountHealth = cache.health(HealthType.init); const zeroAmountRatio = cache.healthRatio(HealthType.init); function cacheAfterPlacingOrder(amount: I80F48) { @@ -601,11 +632,10 @@ export class HealthCache { : adjustedCache.tokenInfos[quoteIndex].balance.isub(amount); adjustedCache.adjustSerum3Reserved( - serum3Market.marketIndex, - serum3Market.baseTokenIndex, + group, + serum3Market, side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48(), ZERO_I80F48(), - serum3Market.quoteTokenIndex, side === Serum3Side.bid ? amount.div(quote.oraclePrice) : ZERO_I80F48(), ZERO_I80F48(), ); @@ -732,6 +762,19 @@ export class Serum3Info { ); } + static emptyFromSerum3Market( + serum3Market: Serum3Market, + baseEntryIndex: number, + quoteEntryIndex: number, + ) { + return new Serum3Info( + ZERO_I80F48(), + baseEntryIndex, + quoteEntryIndex, + serum3Market.marketIndex, + ); + } + healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 { const baseInfo = tokenInfos[this.baseIndex]; const quoteInfo = tokenInfos[this.quoteIndex]; diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index a8590ad71..97f3c034e 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -524,7 +524,7 @@ export class MangoAccount { group, serum3Market, Serum3Side.bid, - I80F48.fromNumber(3), + I80F48.fromNumber(1), ); } @@ -566,7 +566,7 @@ export class MangoAccount { group, serum3Market, Serum3Side.ask, - I80F48.fromNumber(3), + I80F48.fromNumber(1), ); } diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index 47d856dd7..30f5a599c 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -4,6 +4,7 @@ import fs from 'fs'; import { Group } from '../accounts/group'; import { I80F48 } from '../accounts/I80F48'; import { HealthType, MangoAccount } from '../accounts/mangoAccount'; +import { Serum3Market } from '../accounts/serum3'; import { MangoClient } from '../client'; import { MANGO_V4_ID } from '../constants'; import { toUiDecimalsForQuote } from '../utils'; @@ -128,6 +129,29 @@ async function debugUser( getMaxSourceForTokenSwapWrapper(srcToken, tgtToken); } } + + function getMaxForSerum3Wrapper(serum3Market: Serum3Market) { + // if (serum3Market.name !== 'SOL/USDC') return; + console.log( + `getMaxQuoteForSerum3BidUi ${serum3Market.name} ` + + mangoAccount.getMaxQuoteForSerum3BidUi( + group, + serum3Market.serumMarketExternal, + ), + ); + console.log( + `getMaxBaseForSerum3AskUi ${serum3Market.name} ` + + mangoAccount.getMaxBaseForSerum3AskUi( + group, + serum3Market.serumMarketExternal, + ), + ); + } + for (const serum3Market of Array.from( + group.serum3MarketsMapByExternal.values(), + )) { + getMaxForSerum3Wrapper(serum3Market); + } } async function main() { @@ -177,7 +201,7 @@ async function main() { for (const mangoAccount of mangoAccounts) { console.log(`MangoAccount ${mangoAccount.publicKey}`); - // if (mangoAccount.name === '2nd Account') { + // if (mangoAccount.name === 'PnL Test') { await debugUser(client, group, mangoAccount); // } } From 39bdf208132e8706a2b1dc776d13ffa1785d0d1c Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Fri, 23 Sep 2022 09:48:35 +0200 Subject: [PATCH 09/39] ts: Add group.findPerpMarket() --- ts/client/src/accounts/group.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 80ed3ef9a..10dab0eae 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -381,6 +381,12 @@ export class Group { return I80F48.fromNumber(totalAmount); } + public findPerpMarket(marketIndex: number): PerpMarket | undefined { + return Array.from(this.perpMarketsMap.values()).find( + (perpMarket) => perpMarket.perpMarketIndex === marketIndex, + ); + } + public findSerum3Market(marketIndex: number): Serum3Market | undefined { return Array.from(this.serum3MarketsMapByExternal.values()).find( (serum3Market) => serum3Market.marketIndex === marketIndex, From bb6790e678867efe177d1bcaf5ec6ef4a7aa67ae Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Fri, 23 Sep 2022 11:43:26 +0200 Subject: [PATCH 10/39] ts: use price factor in maxSourceForSwap + max perp bid and ask + tests (#237) * ts: use price factor in maxSourceForSwap ts: max perp bid and ask ts: mocha test for max swap Signed-off-by: microwavedcola1 * ts: comemnt Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- programs/mango-v4/src/lib.rs | 2 + programs/mango-v4/src/state/health.rs | 3 + ts/client/src/accounts/bank.ts | 13 +- ts/client/src/accounts/group.ts | 40 ++- ts/client/src/accounts/healthCache.spec.ts | 78 +++++ ts/client/src/accounts/healthCache.ts | 282 ++++++++++++--- ts/client/src/accounts/mangoAccount.ts | 351 +++++++++---------- ts/client/src/accounts/perp.ts | 15 +- ts/client/src/client.ts | 4 +- ts/client/src/debug-scripts/mb-debug-user.ts | 74 ++-- ts/client/src/scripts/devnet-admin.ts | 36 +- ts/client/src/scripts/devnet-user.ts | 221 ++++++++---- 12 files changed, 753 insertions(+), 366 deletions(-) create mode 100644 ts/client/src/accounts/healthCache.spec.ts diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index a0d252e4c..7b58ba6eb 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -460,6 +460,8 @@ pub mod mango_v4 { instructions::perp_close_market(ctx) } + // TODO perp_change_perp_market_params + pub fn perp_deactivate_position(ctx: Context) -> Result<()> { instructions::perp_deactivate_position(ctx) } diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index fdfac4acc..aaff75bf5 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -891,6 +891,8 @@ impl HealthCache { /// swap BTC -> SOL and they're at ui prices of $20000 and $40, that means price /// should be 500000 native_SOL for a native_BTC. Because 1 BTC gives you 500 SOL /// so 1e6 native_BTC gives you 500e9 native_SOL. + /// + /// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here #[cfg(feature = "client")] pub fn max_swap_source_for_health_ratio( &self, @@ -1018,6 +1020,7 @@ impl HealthCache { } #[cfg(feature = "client")] + /// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here pub fn max_perp_for_health_ratio( &self, perp_market_index: PerpMarketIndex, diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index ef1c4078b..7fe584b55 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -10,7 +10,18 @@ export type OracleConfig = { confFilter: I80F48Dto; }; -export class Bank { +export class BankForHealth { + constructor( + public tokenIndex: number, + public maintAssetWeight: I80F48, + public initAssetWeight: I80F48, + public maintLiabWeight: I80F48, + public initLiabWeight: I80F48, + public price: I80F48 | undefined, + ) {} +} + +export class Bank extends BankForHealth { public name: string; public depositIndex: I80F48; public borrowIndex: I80F48; diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 10dab0eae..4c981de96 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -256,7 +256,6 @@ export class Group { bank.oracle, ai, this.getMintDecimals(bank.mint), - this.getMintDecimals(this.insuranceMint), ); bank.price = price; bank.uiPrice = uiPrice; @@ -283,7 +282,6 @@ export class Group { perpMarket.oracle, ai, perpMarket.baseDecimals, - this.getMintDecimals(this.insuranceMint), ); perpMarket.price = price; perpMarket.uiPrice = uiPrice; @@ -295,7 +293,6 @@ export class Group { oracle: PublicKey, ai: AccountInfo, baseDecimals: number, - quoteDecimals: number, ) { let price, uiPrice; if ( @@ -305,13 +302,13 @@ export class Group { ) { const stubOracle = coder.decode('stubOracle', ai.data); price = new I80F48(stubOracle.price.val); - uiPrice = this?.toUiPrice(price, baseDecimals, quoteDecimals); + uiPrice = this?.toUiPrice(price, baseDecimals); } else if (isPythOracle(ai)) { uiPrice = parsePriceData(ai.data).previousPrice; - price = this?.toNativePrice(uiPrice, baseDecimals, quoteDecimals); + price = this?.toNativePrice(uiPrice, baseDecimals); } else if (isSwitchboardOracle(ai)) { uiPrice = await parseSwitchboardOracle(ai); - price = this?.toNativePrice(uiPrice, baseDecimals, quoteDecimals); + price = this?.toNativePrice(uiPrice, baseDecimals); } else { throw new Error( `Unknown oracle provider for oracle ${oracle}, with owner ${ai.owner}`, @@ -347,6 +344,10 @@ export class Group { return banks[0].mintDecimals; } + public getInsuranceMintDecimals(): number { + return this.getMintDecimals(this.insuranceMint); + } + public getFirstBankByMint(mintPk: PublicKey): Bank { const banks = this.banksMapByMint.get(mintPk.toString()); if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`); @@ -478,23 +479,26 @@ export class Group { } } - public toUiPrice( - price: I80F48, - baseDecimals: number, - quoteDecimals: number, - ): number { + public toUiPrice(price: I80F48, baseDecimals: number): number { return price - .mul(I80F48.fromNumber(Math.pow(10, baseDecimals - quoteDecimals))) + .mul( + I80F48.fromNumber( + Math.pow(10, baseDecimals - this.getInsuranceMintDecimals()), + ), + ) .toNumber(); } - public toNativePrice( - uiPrice: number, - baseDecimals: number, - quoteDecimals: number, - ): I80F48 { + public toNativePrice(uiPrice: number, baseDecimals: number): I80F48 { return I80F48.fromNumber(uiPrice).mul( - I80F48.fromNumber(Math.pow(10, quoteDecimals - baseDecimals)), + I80F48.fromNumber( + Math.pow( + 10, + // note: our oracles are quoted in USD and our insurance mint is USD + // please update when these assumptions change + this.getInsuranceMintDecimals() - baseDecimals, + ), + ), ); } diff --git a/ts/client/src/accounts/healthCache.spec.ts b/ts/client/src/accounts/healthCache.spec.ts new file mode 100644 index 000000000..c70732a37 --- /dev/null +++ b/ts/client/src/accounts/healthCache.spec.ts @@ -0,0 +1,78 @@ +import { expect } from 'chai'; +import { toUiDecimalsForQuote } from '../utils'; +import { BankForHealth } from './bank'; +import { HealthCache, TokenInfo } from './healthCache'; +import { I80F48, ZERO_I80F48 } from './I80F48'; + +describe('Health Cache', () => { + it('max swap tokens for min ratio', () => { + // USDC like + const sourceBank = new BankForHealth( + 0, + I80F48.fromNumber(1), + I80F48.fromNumber(1), + I80F48.fromNumber(1), + I80F48.fromNumber(1), + I80F48.fromNumber(1), + ); + // BTC like + const targetBank = new BankForHealth( + 1, + I80F48.fromNumber(0.9), + I80F48.fromNumber(0.8), + I80F48.fromNumber(1.1), + I80F48.fromNumber(1.2), + I80F48.fromNumber(20000), + ); + + const hc = new HealthCache( + [ + new TokenInfo( + 0, + sourceBank.maintAssetWeight, + sourceBank.initAssetWeight, + sourceBank.maintLiabWeight, + sourceBank.initLiabWeight, + sourceBank.price!, + I80F48.fromNumber(-18 * Math.pow(10, 6)), + ZERO_I80F48(), + ), + + new TokenInfo( + 1, + targetBank.maintAssetWeight, + targetBank.initAssetWeight, + targetBank.maintLiabWeight, + targetBank.initLiabWeight, + targetBank.price!, + I80F48.fromNumber(51 * Math.pow(10, 6)), + ZERO_I80F48(), + ), + ], + [], + [], + ); + + expect( + toUiDecimalsForQuote( + hc.getMaxSourceForTokenSwap( + targetBank, + sourceBank, + I80F48.fromNumber(1), + I80F48.fromNumber(0.95), + ), + ).toFixed(3), + ).equals('0.008'); + + expect( + toUiDecimalsForQuote( + hc.getMaxSourceForTokenSwap( + sourceBank, + targetBank, + I80F48.fromNumber(1), + I80F48.fromNumber(0.95), + ), + ).toFixed(3), + ).equals('90.477'); + }); +}); diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 1ffc57a9b..703a987c4 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -1,6 +1,6 @@ import { PublicKey } from '@solana/web3.js'; import _ from 'lodash'; -import { Bank } from './bank'; +import { Bank, BankForHealth } from './bank'; import { Group } from './group'; import { HUNDRED_I80F48, @@ -11,6 +11,7 @@ import { ZERO_I80F48, } from './I80F48'; import { HealthType } from './mangoAccount'; +import { PerpMarket, PerpOrderSide } from './perp'; import { Serum3Market, Serum3Side } from './serum3'; // ░░░░ @@ -45,10 +46,26 @@ export class HealthCache { ) {} static fromDto(dto) { + // console.log( + JSON.stringify( + dto, + function replacer(k, v) { + // console.log(k); + console.log(v); + // if (v instanceof BN) { + // console.log(v); + // return new I80F48(v).toNumber(); + // } + // return v; + }, + 2, + ), + // ); + process.exit(0); return new HealthCache( dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)), dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)), - dto.perpInfos.map((dto) => new PerpInfo(dto)), + dto.perpInfos.map((dto) => PerpInfo.fromDto(dto)), ); } @@ -169,7 +186,7 @@ export class HealthCache { ); } - getOrCreateTokenInfoIndex(bank: Bank): number { + getOrCreateTokenInfoIndex(bank: BankForHealth): number { const index = this.findTokenInfoIndex(bank.tokenIndex); if (index == -1) { this.tokenInfos.push(TokenInfo.emptyFromBank(bank)); @@ -242,6 +259,20 @@ export class HealthCache { serum3Info.reserved = serum3Info.reserved.add(reservedAmount); } + findPerpInfoIndex(perpMarketIndex: number): number { + return this.perpInfos.findIndex( + (perpInfo) => perpInfo.perpMarketIndex === perpMarketIndex, + ); + } + + getOrCreatePerpInfoIndex(perpMarket: PerpMarket): number { + const index = this.findPerpInfoIndex(perpMarket.perpMarketIndex); + if (index == -1) { + this.perpInfos.push(PerpInfo.emptyFromPerpMarket(perpMarket)); + } + return this.findPerpInfoIndex(perpMarket.perpMarketIndex); + } + public static logHealthCache(debug: string, healthCache: HealthCache) { if (debug) console.log(debug); for (const token of healthCache.tokenInfos) { @@ -407,19 +438,17 @@ export class HealthCache { } getMaxSourceForTokenSwap( - group: Group, - sourceMintPk: PublicKey, - targetMintPk: PublicKey, + sourceBank: BankForHealth, + targetBank: BankForHealth, minRatio: I80F48, + priceFactor: I80F48, ): I80F48 { - const sourceBank: Bank = group.getFirstBankByMint(sourceMintPk); - const targetBank: Bank = group.getFirstBankByMint(targetMintPk); - - if (sourceMintPk.equals(targetMintPk)) { - return ZERO_I80F48(); - } - - if (!sourceBank.price || sourceBank.price.lte(ZERO_I80F48())) { + if ( + !sourceBank.price || + sourceBank.price.lte(ZERO_I80F48()) || + !targetBank.price || + targetBank.price.lte(ZERO_I80F48()) + ) { return ZERO_I80F48(); } @@ -441,10 +470,21 @@ export class HealthCache { // - be careful about finding the minRatio point: the function isn't convex const initialRatio = this.healthRatio(HealthType.init); + const initialHealth = this.health(HealthType.init); if (initialRatio.lte(ZERO_I80F48())) { return ZERO_I80F48(); } + // If the price is sufficiently good, then health will just increase from swapping: + // once we've swapped enough, swapping x reduces health by x * source_liab_weight and + // increases it by x * target_asset_weight * price_factor. + const finalHealthSlope = sourceBank.initLiabWeight + .neg() + .add(targetBank.initAssetWeight.mul(priceFactor)); + if (finalHealthSlope.gte(ZERO_I80F48())) { + return MAX_I80F48(); + } + const healthCacheClone: HealthCache = _.cloneDeep(this); const sourceIndex = healthCacheClone.getOrCreateTokenInfoIndex(sourceBank); const targetIndex = healthCacheClone.getOrCreateTokenInfoIndex(targetBank); @@ -461,7 +501,9 @@ export class HealthCache { const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); // HealthCache.logHealthCache('beforeSwap', adjustedCache); adjustedCache.tokenInfos[sourceIndex].balance.isub(amount); - adjustedCache.tokenInfos[targetIndex].balance.iadd(amount); + adjustedCache.tokenInfos[targetIndex].balance.iadd( + amount.mul(priceFactor), + ); // HealthCache.logHealthCache('afterSwap', adjustedCache); return adjustedCache; } @@ -470,11 +512,16 @@ export class HealthCache { return cacheAfterSwap(amount).healthRatio(HealthType.init); } + // There are two key slope changes: Assume source.balance > 0 and target.balance < 0. + // When these values flip sign, the health slope decreases, but could still be positive. + // After point1 it's definitely negative (due to finalHealthSlope check above). + // The maximum health ratio will be at 0 or at one of these points (ignoring serum3 effects). + const sourceForZeroTargetBalance = target.balance.neg().div(priceFactor); const point0Amount = source.balance - .min(target.balance.neg()) + .min(sourceForZeroTargetBalance) .max(ZERO_I80F48()); const point1Amount = source.balance - .max(target.balance.neg()) + .max(sourceForZeroTargetBalance) .max(ZERO_I80F48()); const cache0 = cacheAfterSwap(point0Amount); const point0Ratio = cache0.healthRatio(HealthType.init); @@ -505,12 +552,12 @@ export class HealthCache { // If point1Ratio is still bigger than minRatio, the target amount must be >point1Amount // search to the right of point1Amount: but how far? // At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for - // zero health: health - source_liab_weight * a + target_asset_weight * a = 0. + // zero health: health - source_liab_weight * a + target_asset_weight * a * priceFactor = 0. if (point1Health.lte(ZERO_I80F48())) { return ZERO_I80F48(); } - const zeroHealthAmount = point1Amount.add( - point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)), + const zeroHealthAmount = point1Amount.sub( + point1Health.div(finalHealthSlope), ); const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount); amount = HealthCache.binaryApproximationSearch( @@ -532,21 +579,21 @@ export class HealthCache { healthRatioAfterSwap, ); } else { - throw new Error( - `internal error: assert that init ratio ${initialRatio.toNumber()} <= point0 ratio ${point0Ratio.toNumber()}`, + // Must be between 0 and point0_amount + amount = HealthCache.binaryApproximationSearch( + ZERO_I80F48(), + initialRatio, + point0Amount, + point0Ratio, + minRatio, + healthRatioAfterSwap, ); } - return amount - .div(source.oraclePrice) - .div( - ONE_I80F48().add( - group.getFirstBankByMint(sourceMintPk).loanOriginationFeeRate, - ), - ); + return amount.div(source.oraclePrice); } - getMaxForSerum3Order( + getMaxSerum3OrderForHealthRatio( group: Group, serum3Market: Serum3Market, side: Serum3Side, @@ -669,6 +716,115 @@ export class HealthCache { .div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate)) .div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false)))); } + + getMaxPerpForHealthRatio( + perpMarket: PerpMarket, + side: PerpOrderSide, + minRatio: I80F48, + price: I80F48, + ): I80F48 { + const healthCacheClone: HealthCache = _.cloneDeep(this); + + const initialRatio = this.healthRatio(HealthType.init); + if (initialRatio.lt(ZERO_I80F48())) { + return ZERO_I80F48(); + } + + const direction = side == PerpOrderSide.bid ? 1 : -1; + + const perpInfoIndex = this.getOrCreatePerpInfoIndex(perpMarket); + const perpInfo = this.perpInfos[perpInfoIndex]; + const oraclePrice = perpInfo.oraclePrice; + const baseLotSize = I80F48.fromString(perpMarket.baseLotSize.toString()); + + // If the price is sufficiently good then health will just increase from trading + const finalHealthSlope = + direction == 1 + ? perpInfo.initAssetWeight.mul(oraclePrice).sub(price) + : price.sub(perpInfo.initLiabWeight.mul(oraclePrice)); + if (finalHealthSlope.gte(ZERO_I80F48())) { + return MAX_I80F48(); + } + + function cacheAfterTrade(baseLots: I80F48): HealthCache { + const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); + const d = I80F48.fromNumber(direction); + adjustedCache.perpInfos[perpInfoIndex].base.iadd( + d.mul(baseLots.mul(baseLotSize.mul(oraclePrice))), + ); + adjustedCache.perpInfos[perpInfoIndex].quote.isub( + d.mul(baseLots.mul(baseLotSize.mul(price))), + ); + return adjustedCache; + } + + function healthAfterTrade(baseLots: I80F48): I80F48 { + return cacheAfterTrade(baseLots).health(HealthType.init); + } + function healthRatioAfterTrade(baseLots: I80F48): I80F48 { + return cacheAfterTrade(baseLots).healthRatio(HealthType.init); + } + + const initialBaseLots = perpInfo.base + .div(perpInfo.oraclePrice) + .div(baseLotSize); + + // There are two cases: + // 1. We are increasing abs(baseLots) + // 2. We are bringing the base position to 0, and then going to case 1. + const hasCase2 = + (initialBaseLots.gt(ZERO_I80F48()) && direction == -1) || + (initialBaseLots.lt(ZERO_I80F48()) && direction == 1); + + let case1Start: I80F48, case1StartRatio: I80F48; + if (hasCase2) { + case1Start = initialBaseLots.abs(); + case1StartRatio = healthRatioAfterTrade(case1Start); + } else { + case1Start = ZERO_I80F48(); + case1StartRatio = initialRatio; + } + + // If we start out below minRatio and can't go above, pick the best case + let baseLots: I80F48; + if (initialRatio.lte(minRatio) && case1StartRatio.lt(minRatio)) { + if (case1StartRatio.gte(initialRatio)) { + baseLots = case1Start; + } else { + baseLots = ZERO_I80F48(); + } + } else if (case1StartRatio.gte(minRatio)) { + // Must reach minRatio to the right of case1Start + const case1StartHealth = healthAfterTrade(case1Start); + if (case1StartHealth.lte(ZERO_I80F48())) { + return ZERO_I80F48(); + } + const zeroHealthAmount = case1Start.sub( + case1StartHealth.div(finalHealthSlope).div(baseLotSize), + ); + const zeroHealthRatio = healthRatioAfterTrade(zeroHealthAmount); + baseLots = HealthCache.binaryApproximationSearch( + case1Start, + case1StartRatio, + zeroHealthAmount, + zeroHealthRatio, + minRatio, + healthRatioAfterTrade, + ); + } else { + // Between 0 and case1Start + baseLots = HealthCache.binaryApproximationSearch( + ZERO_I80F48(), + initialRatio, + case1Start, + case1StartRatio, + minRatio, + healthRatioAfterTrade, + ); + } + + return baseLots.floor(); + } } export class TokenInfo { @@ -699,10 +855,10 @@ export class TokenInfo { ); } - static emptyFromBank(bank: Bank): TokenInfo { + static emptyFromBank(bank: BankForHealth): TokenInfo { if (!bank.price) throw new Error( - `Failed to create TokenInfo. Bank price unavailable. ${bank.mint.toString()}`, + `Failed to create TokenInfo. Bank price unavailable for bank with tokenIndex ${bank.tokenIndex}`, ); return new TokenInfo( bank.tokenIndex, @@ -824,22 +980,33 @@ export class Serum3Info { } export class PerpInfo { - constructor(dto: PerpInfoDto) { - this.maintAssetWeight = I80F48.from(dto.maintAssetWeight); - this.initAssetWeight = I80F48.from(dto.initAssetWeight); - this.maintLiabWeight = I80F48.from(dto.maintLiabWeight); - this.initLiabWeight = I80F48.from(dto.initLiabWeight); - this.base = I80F48.from(dto.base); - this.quote = I80F48.from(dto.quote); + constructor( + public perpMarketIndex: number, + public maintAssetWeight: I80F48, + public initAssetWeight: I80F48, + public maintLiabWeight: I80F48, + public initLiabWeight: I80F48, + // in health-reference-token native units, needs scaling by asset/liab + public base: I80F48, + // in health-reference-token native units, no asset/liab factor needed + public quote: I80F48, + public oraclePrice: I80F48, + public hasOpenOrders: boolean, + ) {} + + static fromDto(dto: PerpInfoDto) { + return new PerpInfo( + dto.perpMarketIndex, + I80F48.from(dto.maintAssetWeight), + I80F48.from(dto.initAssetWeight), + I80F48.from(dto.maintLiabWeight), + I80F48.from(dto.initLiabWeight), + I80F48.from(dto.base), + I80F48.from(dto.quote), + I80F48.from(dto.oraclePrice), + dto.hasOpenOrders, + ); } - maintAssetWeight: I80F48; - initAssetWeight: I80F48; - maintLiabWeight: I80F48; - initLiabWeight: I80F48; - // in health-reference-token native units, needs scaling by asset/liab - base: I80F48; - // in health-reference-token native units, no asset/liab factor needed - quote: I80F48; healthContribution(healthType: HealthType): I80F48 { let weight; @@ -859,6 +1026,24 @@ export class PerpInfo { // `self.quote + weight * self.base` here return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48()); } + + static emptyFromPerpMarket(perpMarket: PerpMarket): PerpInfo { + if (!perpMarket.price) + throw new Error( + `Failed to create PerpInfo. Oracle price unavailable. ${perpMarket.oracle.toString()}`, + ); + return new PerpInfo( + perpMarket.perpMarketIndex, + perpMarket.maintAssetWeight, + perpMarket.initAssetWeight, + perpMarket.maintLiabWeight, + perpMarket.initLiabWeight, + ZERO_I80F48(), + ZERO_I80F48(), + I80F48.fromNumber(perpMarket.price), + false, + ); + } } export class HealthCacheDto { @@ -913,6 +1098,7 @@ export class Serum3InfoDto { } export class PerpInfoDto { + perpMarketIndex: number; maintAssetWeight: I80F48Dto; initAssetWeight: I80F48Dto; maintLiabWeight: I80F48Dto; @@ -921,4 +1107,6 @@ export class PerpInfoDto { base: I80F48Dto; // in health-reference-token native units, no asset/liab factor needed quote: I80F48Dto; + oraclePrice: I80F48Dto; + hasOpenOrders: boolean; } diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 97f3c034e..5d53bd19e 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -3,13 +3,18 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { Order, Orderbook } from '@project-serum/serum/lib/market'; import { PublicKey } from '@solana/web3.js'; import { MangoClient } from '../client'; -import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils'; +import { + nativeI80F48ToUi, + toNative, + toUiDecimals, + toUiDecimalsForQuote, +} from '../utils'; import { Bank } from './bank'; import { Group } from './group'; import { HealthCache, HealthCacheDto } from './healthCache'; import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48'; -import { PerpOrder } from './perp'; -import { Serum3Market, Serum3Side } from './serum3'; +import { PerpOrder, PerpOrderSide } from './perp'; +import { Serum3Side } from './serum3'; export class MangoAccount { public tokens: TokenPosition[]; public serum3: Serum3Orders[]; @@ -310,13 +315,6 @@ export class MangoAccount { // can withdraw without borrowing until initHealth reaches 0 if (existingPositionHealthContrib.gt(initHealth)) { const withdrawAbleExistingPositionHealthContrib = initHealth; - // console.log(`initHealth ${initHealth}`); - // console.log( - // `existingPositionHealthContrib ${existingPositionHealthContrib}`, - // ); - // console.log( - // `withdrawAbleExistingPositionHealthContrib ${withdrawAbleExistingPositionHealthContrib}`, - // ); return withdrawAbleExistingPositionHealthContrib .div(tokenBank.initAssetWeight) .div(tokenBank.price); @@ -332,15 +330,6 @@ export class MangoAccount { const maxBorrowNativeWithoutFees = maxBorrowNative.div( ONE_I80F48().add(tokenBank.loanOriginationFeeRate), ); - // console.log(`initHealth ${initHealth}`); - // console.log( - // `existingPositionHealthContrib ${existingPositionHealthContrib}`, - // ); - // console.log( - // `initHealthWithoutExistingPosition ${initHealthWithoutExistingPosition}`, - // ); - // console.log(`maxBorrowNative ${maxBorrowNative}`); - // console.log(`maxBorrowNativeWithoutFees ${maxBorrowNativeWithoutFees}`); return maxBorrowNativeWithoutFees.add(existingTokenDeposits); } @@ -359,78 +348,46 @@ export class MangoAccount { } } - /** - * The max amount of given source native token you can swap to a target token. - * note: slippageAndFeesFactor is a normalized number, <1, - * e.g. a slippage of 5% and some fees which are 1%, then slippageAndFeesFactor = 0.94 - * the factor is used to compute how much target can be obtained by swapping source - * @returns max amount of given source native token you can swap to a target token, in native token - */ - getMaxSourceForTokenSwap( - group: Group, - sourceMintPk: PublicKey, - targetMintPk: PublicKey, - slippageAndFeesFactor: number, - ): I80F48 | undefined { - if (!this.accountData) return undefined; - return this.accountData.healthCache - .getMaxSourceForTokenSwap( - group, - sourceMintPk, - targetMintPk, - ONE_I80F48(), // target 1% health - ) - .mul(I80F48.fromNumber(slippageAndFeesFactor)); - } - /** * The max amount of given source ui token you can swap to a target token. - * note: slippageAndFeesFactor is a normalized number, <1, - * e.g. a slippage of 5% and some fees which are 1%, then slippageAndFeesFactor = 0.94 + * PriceFactor is ratio between A - how many source tokens can be traded for target tokens + * and B - source native oracle price / target native oracle price. + * e.g. a slippage of 5% and some fees which are 1%, then priceFactor = 0.94 * the factor is used to compute how much target can be obtained by swapping source + * in reality, and not only relying on oracle prices, and taking in account e.g. slippage which + * can occur at large size * @returns max amount of given source ui token you can swap to a target token, in ui token */ getMaxSourceUiForTokenSwap( group: Group, sourceMintPk: PublicKey, targetMintPk: PublicKey, - slippageAndFeesFactor: number, + priceFactor: number, ): number | undefined { - const maxSource = this.getMaxSourceForTokenSwap( - group, - sourceMintPk, - targetMintPk, - slippageAndFeesFactor, + if (!this.accountData) { + throw new Error( + `accountData not loaded on MangoAccount, try reloading MangoAccount`, + ); + } + if (sourceMintPk.equals(targetMintPk)) { + return 0; + } + const maxSource = this.accountData.healthCache.getMaxSourceForTokenSwap( + group.getFirstBankByMint(sourceMintPk), + group.getFirstBankByMint(targetMintPk), + ONE_I80F48(), // target 1% health + I80F48.fromNumber(priceFactor), + ); + maxSource.idiv( + ONE_I80F48().add( + group.getFirstBankByMint(sourceMintPk).loanOriginationFeeRate, + ), ); if (maxSource) { return toUiDecimals(maxSource, group.getMintDecimals(sourceMintPk)); } } - /** - * Simulates new health ratio after applying tokenChanges to the token positions. - * Note: token changes are expected in native amounts - * - * e.g. useful to simulate health after a potential swap. - * Note: health ratio is technically ∞ if liabs are 0 - * @returns health ratio, in percentage form - */ - simHealthRatioWithTokenPositionChanges( - group: Group, - nativeTokenChanges: { - nativeTokenAmount: I80F48; - mintPk: PublicKey; - }[], - healthType: HealthType = HealthType.init, - ): I80F48 | undefined { - if (!this.accountData) return undefined; - return this.accountData.healthCache.simHealthRatioWithTokenPositionChanges( - group, - nativeTokenChanges, - healthType, - ); - } - /** * Simulates new health ratio after applying tokenChanges to the token positions. * Note: token changes are expected in ui amounts @@ -506,28 +463,11 @@ export class MangoAccount { } /** - * + * TODO priceFactor * @param group - * @param serum3Market - * @returns maximum native quote which can be traded for base token given current health + * @param externalMarketPk + * @returns maximum ui quote which can be traded for base token given current health */ - public getMaxQuoteForSerum3Bid( - group: Group, - serum3Market: Serum3Market, - ): I80F48 { - if (!this.accountData) { - throw new Error( - `accountData not loaded on MangoAccount, try reloading MangoAccount`, - ); - } - return this.accountData.healthCache.getMaxForSerum3Order( - group, - serum3Market, - Serum3Side.bid, - I80F48.fromNumber(1), - ); - } - public getMaxQuoteForSerum3BidUi( group: Group, externalMarketPk: PublicKey, @@ -540,7 +480,18 @@ export class MangoAccount { `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, ); } - const nativeAmount = this.getMaxQuoteForSerum3Bid(group, serum3Market); + if (!this.accountData) { + throw new Error( + `accountData not loaded on MangoAccount, try reloading MangoAccount`, + ); + } + const nativeAmount = + this.accountData.healthCache.getMaxSerum3OrderForHealthRatio( + group, + serum3Market, + Serum3Side.bid, + I80F48.fromNumber(1), + ); return toUiDecimals( nativeAmount, group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals, @@ -548,28 +499,11 @@ export class MangoAccount { } /** - * + * TODO priceFactor * @param group - * @param serum3Market - * @returns maximum native base which can be traded for quote token given current health + * @param externalMarketPk + * @returns maximum ui base which can be traded for quote token given current health */ - public getMaxBaseForSerum3Ask( - group: Group, - serum3Market: Serum3Market, - ): I80F48 { - if (!this.accountData) { - throw new Error( - `accountData not loaded on MangoAccount, try reloading MangoAccount`, - ); - } - return this.accountData.healthCache.getMaxForSerum3Order( - group, - serum3Market, - Serum3Side.ask, - I80F48.fromNumber(1), - ); - } - public getMaxBaseForSerum3AskUi( group: Group, externalMarketPk: PublicKey, @@ -582,7 +516,18 @@ export class MangoAccount { `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, ); } - const nativeAmount = this.getMaxBaseForSerum3Ask(group, serum3Market); + if (!this.accountData) { + throw new Error( + `accountData not loaded on MangoAccount, try reloading MangoAccount`, + ); + } + const nativeAmount = + this.accountData.healthCache.getMaxSerum3OrderForHealthRatio( + group, + serum3Market, + Serum3Side.ask, + I80F48.fromNumber(1), + ); return toUiDecimals( nativeAmount, group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals, @@ -592,31 +537,12 @@ export class MangoAccount { /** * * @param group - * @param nativeQuoteAmount - * @param serum3Market + * @param uiQuoteAmount + * @param externalMarketPk * @param healthType - * @returns health ratio after a bid with nativeQuoteAmount is placed + * @returns health ratio after a bid with uiQuoteAmount is placed */ - simHealthRatioWithSerum3BidChanges( - group: Group, - nativeQuoteAmount: I80F48, - serum3Market: Serum3Market, - healthType: HealthType = HealthType.init, - ): I80F48 { - if (!this.accountData) { - throw new Error( - `accountData not loaded on MangoAccount, try reloading MangoAccount`, - ); - } - return this.accountData.healthCache.simHealthRatioWithSerum3BidChanges( - group, - nativeQuoteAmount, - serum3Market, - healthType, - ); - } - - simHealthRatioWithSerum3BidUiChanges( + public simHealthRatioWithSerum3BidUiChanges( group: Group, uiQuoteAmount: number, externalMarketPk: PublicKey, @@ -630,46 +556,34 @@ export class MangoAccount { `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, ); } - return this.simHealthRatioWithSerum3BidChanges( - group, - toNative( - uiQuoteAmount, - group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) - .mintDecimals, - ), - serum3Market, - healthType, - ).toNumber(); - } - - /** - * - * @param group - * @param nativeBaseAmount - * @param serum3Market - * @param healthType - * @returns health ratio after an ask with nativeBaseAmount is placed - */ - simHealthRatioWithSerum3AskChanges( - group: Group, - nativeBaseAmount: I80F48, - serum3Market: Serum3Market, - healthType: HealthType = HealthType.init, - ): I80F48 { if (!this.accountData) { throw new Error( `accountData not loaded on MangoAccount, try reloading MangoAccount`, ); } - return this.accountData.healthCache.simHealthRatioWithSerum3AskChanges( - group, - nativeBaseAmount, - serum3Market, - healthType, - ); + return this.accountData.healthCache + .simHealthRatioWithSerum3BidChanges( + group, + toNative( + uiQuoteAmount, + group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) + .mintDecimals, + ), + serum3Market, + healthType, + ) + .toNumber(); } - simHealthRatioWithSerum3AskUiChanges( + /** + * + * @param group + * @param uiBaseAmount + * @param externalMarketPk + * @param healthType + * @returns health ratio after an ask with uiBaseAmount is placed + */ + public simHealthRatioWithSerum3AskUiChanges( group: Group, uiBaseAmount: number, externalMarketPk: PublicKey, @@ -683,16 +597,87 @@ export class MangoAccount { `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, ); } - return this.simHealthRatioWithSerum3AskChanges( - group, - toNative( - uiBaseAmount, - group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) - .mintDecimals, - ), - serum3Market, - healthType, - ).toNumber(); + if (!this.accountData) { + throw new Error( + `accountData not loaded on MangoAccount, try reloading MangoAccount`, + ); + } + return this.accountData.healthCache + .simHealthRatioWithSerum3AskChanges( + group, + toNative( + uiBaseAmount, + group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) + .mintDecimals, + ), + serum3Market, + healthType, + ) + .toNumber(); + } + + /** + * + * @param group + * @param perpMarketName + * @param uiPrice ui price at which bid would be placed at + * @returns max ui quote bid + */ + public getMaxQuoteForPerpBidUi( + group: Group, + perpMarketName: string, + uiPrice: number, + ): number { + const perpMarket = group.perpMarketsMap.get(perpMarketName); + if (!perpMarket) { + throw new Error(`PerpMarket for ${perpMarketName} not found!`); + } + if (!this.accountData) { + throw new Error( + `accountData not loaded on MangoAccount, try reloading MangoAccount`, + ); + } + const baseLots = this.accountData.healthCache.getMaxPerpForHealthRatio( + perpMarket, + PerpOrderSide.bid, + I80F48.fromNumber(1), + group.toNativePrice(uiPrice, perpMarket.baseDecimals), + ); + const nativeBase = baseLots.mul( + I80F48.fromString(perpMarket.baseLotSize.toString()), + ); + const nativeQuote = nativeBase.mul(I80F48.fromNumber(perpMarket.price)); + return toUiDecimalsForQuote(nativeQuote.toNumber()); + } + + /** + * + * @param group + * @param perpMarketName + * @param uiPrice ui price at which ask would be placed at + * @returns max ui base ask + */ + public getMaxBaseForPerpAskUi( + group: Group, + perpMarketName: string, + uiPrice: number, + ): number { + const perpMarket = group.perpMarketsMap.get(perpMarketName); + if (!perpMarket) { + throw new Error(`PerpMarket for ${perpMarketName} not found!`); + } + if (!this.accountData) { + throw new Error( + `accountData not loaded on MangoAccount, try reloading MangoAccount`, + ); + } + const baseLots = this.accountData.healthCache.getMaxPerpForHealthRatio( + perpMarket, + PerpOrderSide.ask, + I80F48.fromNumber(1), + group.toNativePrice(uiPrice, perpMarket.baseDecimals), + ); + return perpMarket.baseLotsToUi(new BN(baseLots.toString())); } public async loadPerpOpenOrdersForMarket( diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index 9a0b3875f..41a9ce68d 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -364,7 +364,7 @@ export class BookSide { for (const order of this.items()) { s.iadd(order.sizeLots); if (s.gte(baseLots)) { - return order.price; + return order.uiPrice; } } return undefined; @@ -391,7 +391,7 @@ export class BookSide { public getL2Ui(depth: number): [number, number][] { const levels: [number, number][] = []; - for (const { price, size } of this.items()) { + for (const { uiPrice: price, uiSize: size } of this.items()) { if (levels.length > 0 && levels[levels.length - 1][0] === price) { levels[levels.length - 1][1] += size; } else if (levels.length === depth) { @@ -463,7 +463,7 @@ export class InnerNode { constructor(public children: [number]) {} } -export class Side { +export class PerpOrderSide { static bid = { bid: {} }; static ask = { ask: {} }; } @@ -478,7 +478,8 @@ export class PerpOrderType { export class PerpOrder { static from(perpMarket: PerpMarket, leafNode: LeafNode, type: BookSideType) { - const side = type == BookSideType.bids ? Side.bid : Side.ask; + const side = + type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask; const price = BookSide.getPriceFromKey(leafNode.key); const expiryTimestamp = leafNode.timeInForce ? leafNode.timestamp.add(new BN(leafNode.timeInForce)) @@ -506,11 +507,11 @@ export class PerpOrder { public owner: PublicKey, public openOrdersSlot: number, public feeTier: 0, - public price: number, + public uiPrice: number, public priceLots: BN, - public size: number, + public uiSize: number, public sizeLots: BN, - public side: Side, + public side: PerpOrderSide, public timestamp: BN, public expiryTimestamp: BN, ) {} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 289464ed3..3d91b7cc9 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -34,7 +34,7 @@ import { PerpPosition, } from './accounts/mangoAccount'; import { StubOracle } from './accounts/oracle'; -import { PerpMarket, PerpOrderType, Side } from './accounts/perp'; +import { PerpMarket, PerpOrderSide, PerpOrderType } from './accounts/perp'; import { generateSerum3MarketExternalVaultSignerAddress, Serum3Market, @@ -1504,7 +1504,7 @@ export class MangoClient { group: Group, mangoAccount: MangoAccount, perpMarketName: string, - side: Side, + side: PerpOrderSide, price: number, quantity: number, maxQuoteQuantity: number, diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index 30f5a599c..36d5395e4 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -1,14 +1,24 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; -import { Connection, Keypair } from '@solana/web3.js'; +import { Cluster, Connection, Keypair } from '@solana/web3.js'; import fs from 'fs'; import { Group } from '../accounts/group'; -import { I80F48 } from '../accounts/I80F48'; import { HealthType, MangoAccount } from '../accounts/mangoAccount'; +import { PerpMarket } from '../accounts/perp'; import { Serum3Market } from '../accounts/serum3'; import { MangoClient } from '../client'; import { MANGO_V4_ID } from '../constants'; import { toUiDecimalsForQuote } from '../utils'; +const CLUSTER_URL = + process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL; +const PAYER_KEYPAIR = + process.env.PAYER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR; +const USER_KEYPAIR = + process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR; +const GROUP_NUM = Number(process.env.GROUP_NUM || 2); +const CLUSTER: Cluster = + (process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta'; + async function debugUser( client: MangoClient, group: Group, @@ -107,19 +117,12 @@ async function debugUser( function getMaxSourceForTokenSwapWrapper(src, tgt) { console.log( `getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` + - mangoAccount - .getMaxSourceForTokenSwap( - group, - group.banksMapByName.get(src)![0].mint, - group.banksMapByName.get(tgt)![0].mint, - 1, - )! - .div( - I80F48.fromNumber( - Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals), - ), - ) - .toNumber(), + mangoAccount.getMaxSourceUiForTokenSwap( + group, + group.banksMapByName.get(src)![0].mint, + group.banksMapByName.get(tgt)![0].mint, + 1, + ), ); } for (const srcToken of Array.from(group.banksMapByName.keys())) { @@ -130,6 +133,28 @@ async function debugUser( } } + function getMaxForPerpWrapper(perpMarket: PerpMarket) { + console.log( + `getMaxQuoteForPerpBidUi ${perpMarket.name} ` + + mangoAccount.getMaxQuoteForPerpBidUi( + group, + perpMarket.name, + perpMarket.price, + ), + ); + console.log( + `getMaxBaseForPerpAskUi ${perpMarket.name} ` + + mangoAccount.getMaxBaseForPerpAskUi( + group, + perpMarket.name, + perpMarket.price, + ), + ); + } + for (const perpMarket of Array.from(group.perpMarketsMap.values())) { + getMaxForPerpWrapper(perpMarket); + } + function getMaxForSerum3Wrapper(serum3Market: Serum3Market) { // if (serum3Market.name !== 'SOL/USDC') return; console.log( @@ -156,12 +181,10 @@ async function debugUser( async function main() { const options = AnchorProvider.defaultOptions(); - const connection = new Connection(process.env.MB_CLUSTER_URL!, options); + const connection = new Connection(CLUSTER_URL!, options); const admin = Keypair.fromSecretKey( - Buffer.from( - JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')), - ), + Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR!, 'utf-8'))), ); console.log(`Admin ${admin.publicKey.toBase58()}`); @@ -169,16 +192,15 @@ async function main() { const adminProvider = new AnchorProvider(connection, adminWallet, options); const client = MangoClient.connect( adminProvider, - 'mainnet-beta', - MANGO_V4_ID['mainnet-beta'], + CLUSTER, + MANGO_V4_ID[CLUSTER], + {}, + 'get-program-accounts', ); - const group = await client.getGroupForCreator(admin.publicKey, 2); + const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); - for (const keypair of [ - process.env.MB_PAYER_KEYPAIR!, - process.env.MB_USER2_KEYPAIR!, - ]) { + for (const keypair of [USER_KEYPAIR!]) { console.log(); const user = Keypair.fromSecretKey( Buffer.from(JSON.parse(fs.readFileSync(keypair, 'utf-8'))), diff --git a/ts/client/src/scripts/devnet-admin.ts b/ts/client/src/scripts/devnet-admin.ts index d6e9c7633..f0b0369d6 100644 --- a/ts/client/src/scripts/devnet-admin.ts +++ b/ts/client/src/scripts/devnet-admin.ts @@ -357,7 +357,6 @@ async function main() { 0, 'BTC-PERP', 0.1, - 1, 6, 1, 10, @@ -372,15 +371,14 @@ async function main() { 0.05, 0.05, 100, + true, + true, ); console.log('done'); } catch (error) { console.log(error); } - const perpMarkets = await client.perpGetMarkets( - group, - group.getFirstBankByMint(btcDevnetMint).tokenIndex, - ); + const perpMarkets = await client.perpGetMarkets(group); console.log(`...created perp market ${perpMarkets[0].publicKey}`); // @@ -480,6 +478,34 @@ async function main() { } catch (error) { throw error; } + + console.log(`Editing BTC-PERP...`); + try { + let sig = await client.perpEditMarket( + group, + 'BTC-PERP', + btcDevnetOracle, + 0.1, + 6, + 0.975, + 0.95, + 1.025, + 1.05, + 0.012, + 0.0002, + 0.0, + 0.05, + 0.05, + 100, + true, + true, + ); + console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); + await group.reloadAll(client); + console.log(group.getFirstBankByMint(btcDevnetMint).toString()); + } catch (error) { + throw error; + } } process.exit(); diff --git a/ts/client/src/scripts/devnet-user.ts b/ts/client/src/scripts/devnet-user.ts index 6afca4019..8b3499ff2 100644 --- a/ts/client/src/scripts/devnet-user.ts +++ b/ts/client/src/scripts/devnet-user.ts @@ -1,7 +1,6 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; -import { I80F48 } from '../accounts/I80F48'; import { HealthType } from '../accounts/mangoAccount'; import { BookSide, PerpOrderType, Side } from '../accounts/perp'; import { @@ -350,19 +349,12 @@ async function main() { // console.log(); console.log( `getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` + - mangoAccount - .getMaxSourceForTokenSwap( - group, - group.banksMapByName.get(src)![0].mint, - group.banksMapByName.get(tgt)![0].mint, - 1, - )! - .div( - I80F48.fromNumber( - Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals), - ), - ) - .toNumber(), + mangoAccount.getMaxSourceUiForTokenSwap( + group, + group.banksMapByName.get(src)![0].mint, + group.banksMapByName.get(tgt)![0].mint, + 1, + )!, ); } for (const srcToken of Array.from(group.banksMapByName.keys())) { @@ -407,39 +399,45 @@ async function main() { // perps if (true) { + let sig; const orders = await mangoAccount.loadPerpOpenOrdersForMarket( client, group, 'BTC-PERP', ); for (const order of orders) { - console.log(`Current order - ${order.price} ${order.size} ${order.side}`); + console.log( + `Current order - ${order.uiPrice} ${order.uiSize} ${order.side}`, + ); } console.log(`...cancelling all perp orders`); - let sig = await client.perpCancelAllOrders( - group, - mangoAccount, - 'BTC-PERP', - 10, - ); + sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); // scenario 1 - // not going to be hit orders, far from each other + // bid max perp try { const clientId = Math.floor(Math.random() * 99999); const price = group.banksMapByName.get('BTC')![0].uiPrice! - Math.floor(Math.random() * 100); - console.log(`...placing perp bid ${clientId} at ${price}`); + const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi( + group, + 'BTC-PERP', + 1, + ); + const baseQty = quoteQty / price; + console.log( + `...placing max qty perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, + ); const sig = await client.perpPlaceOrder( group, mangoAccount, 'BTC-PERP', Side.bid, price, - 0.01, - price * 0.01, + baseQty, + quoteQty, clientId, PerpOrderType.limit, 0, //Date.now() + 200, @@ -449,20 +447,59 @@ async function main() { } catch (error) { console.log(error); } + console.log(`...cancelling all perp orders`); + sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); + console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + + // bid max perp + some + try { + const clientId = Math.floor(Math.random() * 99999); + const price = + group.banksMapByName.get('BTC')![0].uiPrice! - + Math.floor(Math.random() * 100); + const quoteQty = + mangoAccount.getMaxQuoteForPerpBidUi(group, 'BTC-PERP', 1) * 1.02; + const baseQty = quoteQty / price; + console.log( + `...placing max qty * 1.02 perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, + ); + const sig = await client.perpPlaceOrder( + group, + mangoAccount, + 'BTC-PERP', + Side.bid, + price, + baseQty, + quoteQty, + clientId, + PerpOrderType.limit, + 0, //Date.now() + 200, + 1, + ); + console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + } catch (error) { + console.log('Errored out as expected'); + } + + // bid max ask try { const clientId = Math.floor(Math.random() * 99999); const price = group.banksMapByName.get('BTC')![0].uiPrice! + Math.floor(Math.random() * 100); - console.log(`...placing perp ask ${clientId} at ${price}`); + const baseQty = mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1); + const quoteQty = baseQty * price; + console.log( + `...placing max qty perp ask clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, + ); const sig = await client.perpPlaceOrder( group, mangoAccount, 'BTC-PERP', Side.ask, price, - 0.01, - price * 0.01, + baseQty, + quoteQty, clientId, PerpOrderType.limit, 0, //Date.now() + 200, @@ -472,59 +509,89 @@ async function main() { } catch (error) { console.log(error); } - // should be able to cancel them + + // bid max ask + some + try { + const clientId = Math.floor(Math.random() * 99999); + const price = + group.banksMapByName.get('BTC')![0].uiPrice! + + Math.floor(Math.random() * 100); + const baseQty = + mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1) * 1.02; + const quoteQty = baseQty * price; + console.log( + `...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, + ); + const sig = await client.perpPlaceOrder( + group, + mangoAccount, + 'BTC-PERP', + Side.ask, + price, + baseQty, + quoteQty, + clientId, + PerpOrderType.limit, + 0, //Date.now() + 200, + 1, + ); + console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + } catch (error) { + console.log('Errored out as expected'); + } + console.log(`...cancelling all perp orders`); sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - // scenario 2 - // make + take orders - try { - const clientId = Math.floor(Math.random() * 99999); - const price = group.banksMapByName.get('BTC')![0].uiPrice!; - console.log(`...placing perp bid ${clientId} at ${price}`); - const sig = await client.perpPlaceOrder( - group, - mangoAccount, - 'BTC-PERP', - Side.bid, - price, - 0.01, - price * 0.01, - clientId, - PerpOrderType.limit, - 0, //Date.now() + 200, - 1, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - } catch (error) { - console.log(error); - } - try { - const clientId = Math.floor(Math.random() * 99999); - const price = group.banksMapByName.get('BTC')![0].uiPrice!; - console.log(`...placing perp ask ${clientId} at ${price}`); - const sig = await client.perpPlaceOrder( - group, - mangoAccount, - 'BTC-PERP', - Side.ask, - price, - 0.01, - price * 0.011, - clientId, - PerpOrderType.limit, - 0, //Date.now() + 200, - 1, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - } catch (error) { - console.log(error); - } - // // should be able to cancel them : know bug - // console.log(`...cancelling all perp orders`); - // sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); - // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // // scenario 2 + // // make + take orders + // try { + // const clientId = Math.floor(Math.random() * 99999); + // const price = group.banksMapByName.get('BTC')![0].uiPrice!; + // console.log(`...placing perp bid ${clientId} at ${price}`); + // const sig = await client.perpPlaceOrder( + // group, + // mangoAccount, + // 'BTC-PERP', + // Side.bid, + // price, + // 0.01, + // price * 0.01, + // clientId, + // PerpOrderType.limit, + // 0, //Date.now() + 200, + // 1, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // } catch (error) { + // console.log(error); + // } + // try { + // const clientId = Math.floor(Math.random() * 99999); + // const price = group.banksMapByName.get('BTC')![0].uiPrice!; + // console.log(`...placing perp ask ${clientId} at ${price}`); + // const sig = await client.perpPlaceOrder( + // group, + // mangoAccount, + // 'BTC-PERP', + // Side.ask, + // price, + // 0.01, + // price * 0.011, + // clientId, + // PerpOrderType.limit, + // 0, //Date.now() + 200, + // 1, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // } catch (error) { + // console.log(error); + // } + // // // should be able to cancel them : know bug + // // console.log(`...cancelling all perp orders`); + // // sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); + // // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); const perpMarket = group.perpMarketsMap.get('BTC-PERP'); From f6c9a93ac5a217e025045023d05f6a482b0d0bf3 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Fri, 23 Sep 2022 12:04:14 +0200 Subject: [PATCH 11/39] Health: Avoid overflow panic in health ratio for tiny liabs Spotted by microwavedcola --- programs/mango-v4/src/state/health.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index aaff75bf5..e73ed28be 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -878,7 +878,8 @@ impl HealthCache { let (assets, liabs) = self.health_assets_and_liabs(health_type); let hundred = I80F48::from(100); if liabs > 0 { - cm!(hundred * (assets - liabs) / liabs) + // feel free to saturate to MAX for tiny liabs + cm!(hundred * (assets - liabs)).saturating_div(liabs) } else { I80F48::MAX } From 1320451e1ae5032188cf493fbd0255af5af33b0b Mon Sep 17 00:00:00 2001 From: Nicholas Clarke Date: Fri, 23 Sep 2022 10:42:43 -0700 Subject: [PATCH 12/39] Add serum open orders balance logging. Remove price from token and perp balances. (#236) --- .../mango-v4/src/instructions/flash_loan.rs | 1 - .../src/instructions/perp_consume_events.rs | 3 -- .../instructions/serum3_cancel_all_orders.rs | 19 ++++++++ .../src/instructions/serum3_cancel_order.rs | 19 ++++++++ .../serum3_liq_force_cancel_orders.rs | 19 +++++++- .../src/instructions/serum3_place_order.rs | 41 ++++++++++++++++ .../src/instructions/serum3_settle_funds.rs | 18 +++++++ .../src/instructions/token_deposit.rs | 1 - .../src/instructions/token_liq_bankruptcy.rs | 5 +- .../src/instructions/token_liq_with_token.rs | 4 -- .../src/instructions/token_withdraw.rs | 1 - programs/mango-v4/src/logs.rs | 14 ++---- ts/client/src/mango_v4.ts | 48 ++++++------------- 13 files changed, 135 insertions(+), 58 deletions(-) diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index c52981e7d..05de67c06 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -377,7 +377,6 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( indexed_position: position.indexed_position.to_bits(), deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), - price: price.to_bits(), }); } diff --git a/programs/mango-v4/src/instructions/perp_consume_events.rs b/programs/mango-v4/src/instructions/perp_consume_events.rs index 27bca076e..3d957dbe4 100644 --- a/programs/mango-v4/src/instructions/perp_consume_events.rs +++ b/programs/mango-v4/src/instructions/perp_consume_events.rs @@ -64,7 +64,6 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res ctx.accounts.group.key(), fill.maker, perp_market.perp_market_index as u64, - fill.price, ma.perp_position(perp_market.perp_market_index).unwrap(), &perp_market, ); @@ -105,7 +104,6 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res ctx.accounts.group.key(), fill.maker, perp_market.perp_market_index as u64, - fill.price, maker.perp_position(perp_market.perp_market_index).unwrap(), &perp_market, ); @@ -113,7 +111,6 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res ctx.accounts.group.key(), fill.taker, perp_market.perp_market_index as u64, - fill.price, taker.perp_position(perp_market.perp_market_index).unwrap(), &perp_market, ); diff --git a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs index 0ac3b5ae0..6ac6f95ae 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs @@ -1,6 +1,9 @@ use anchor_lang::prelude::*; +use super::{OpenOrdersAmounts, OpenOrdersSlim}; use crate::error::*; +use crate::logs::Serum3OpenOrdersBalanceLog; +use crate::serum3_cpi::load_open_orders_ref; use crate::state::*; #[derive(Accounts)] @@ -72,6 +75,22 @@ pub fn serum3_cancel_all_orders(ctx: Context, limit: u8) // cpi_cancel_all_orders(ctx.accounts, limit)?; + let serum_market = ctx.accounts.serum_market.load()?; + let oo_ai = &ctx.accounts.open_orders.as_ref(); + let open_orders = load_open_orders_ref(oo_ai)?; + let after_oo = OpenOrdersSlim::from_oo(&open_orders); + emit!(Serum3OpenOrdersBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + base_token_index: serum_market.base_token_index, + quote_token_index: serum_market.quote_token_index, + base_total: after_oo.native_base_total(), + base_free: after_oo.native_base_free(), + quote_total: after_oo.native_quote_total(), + quote_free: after_oo.native_quote_free(), + referrer_rebates_accrued: after_oo.native_rebates(), + }); + Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_cancel_order.rs b/programs/mango-v4/src/instructions/serum3_cancel_order.rs index 7b4af32f5..89645c1f9 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_order.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_order.rs @@ -7,6 +7,10 @@ use crate::state::*; use super::Serum3Side; +use super::{OpenOrdersAmounts, OpenOrdersSlim}; +use crate::logs::Serum3OpenOrdersBalanceLog; +use crate::serum3_cpi::load_open_orders_ref; + #[derive(Accounts)] pub struct Serum3CancelOrder<'info> { pub group: AccountLoader<'info, Group>, @@ -85,6 +89,21 @@ pub fn serum3_cancel_order( }; cpi_cancel_order(ctx.accounts, order)?; + let oo_ai = &ctx.accounts.open_orders.as_ref(); + let open_orders = load_open_orders_ref(oo_ai)?; + let after_oo = OpenOrdersSlim::from_oo(&open_orders); + emit!(Serum3OpenOrdersBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + base_token_index: serum_market.base_token_index, + quote_token_index: serum_market.quote_token_index, + base_total: after_oo.native_base_total(), + base_free: after_oo.native_base_free(), + quote_total: after_oo.native_quote_total(), + quote_free: after_oo.native_quote_free(), + referrer_rebates_accrued: after_oo.native_rebates(), + }); + Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 448c44afd..d54d5aac6 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -4,8 +4,10 @@ use fixed::types::I80F48; use crate::error::*; use crate::instructions::{ - apply_vault_difference, charge_loan_origination_fees, OODifference, OpenOrdersSlim, + apply_vault_difference, charge_loan_origination_fees, OODifference, OpenOrdersAmounts, + OpenOrdersSlim, }; +use crate::logs::Serum3OpenOrdersBalanceLog; use crate::serum3_cpi::load_open_orders_ref; use crate::state::*; @@ -178,6 +180,19 @@ pub fn serum3_liq_force_cancel_orders( let oo_ai = &ctx.accounts.open_orders.as_ref(); let open_orders = load_open_orders_ref(oo_ai)?; let after_oo = OpenOrdersSlim::from_oo(&open_orders); + + emit!(Serum3OpenOrdersBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + base_token_index: serum_market.base_token_index, + quote_token_index: serum_market.quote_token_index, + base_total: after_oo.native_base_total(), + base_free: after_oo.native_base_free(), + quote_total: after_oo.native_quote_total(), + quote_free: after_oo.native_quote_free(), + referrer_rebates_accrued: after_oo.native_rebates(), + }); + OODifference::new(&before_oo, &after_oo) .adjust_health_cache(&mut health_cache, &serum_market)?; }; @@ -196,6 +211,7 @@ pub fn serum3_liq_force_cancel_orders( let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; apply_vault_difference( + ctx.accounts.account.key(), &mut account.borrow_mut(), serum_market.market_index, &mut base_bank, @@ -204,6 +220,7 @@ pub fn serum3_liq_force_cancel_orders( )? .adjust_health_cache(&mut health_cache)?; apply_vault_difference( + ctx.accounts.account.key(), &mut account.borrow_mut(), serum_market.market_index, &mut quote_bank, diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index e3c60d397..953c4e2ee 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -1,5 +1,6 @@ use crate::error::*; +use crate::logs::{Serum3OpenOrdersBalanceLog, TokenBalanceLog}; use crate::serum3_cpi::{load_market_state, load_open_orders_ref}; use crate::state::*; use anchor_lang::prelude::*; @@ -39,7 +40,9 @@ pub trait OpenOrdersAmounts { fn native_quote_free(&self) -> u64; fn native_quote_free_plus_rebates(&self) -> u64; fn native_base_total(&self) -> u64; + fn native_quote_total(&self) -> u64; fn native_quote_total_plus_rebates(&self) -> u64; + fn native_rebates(&self) -> u64; } impl OpenOrdersAmounts for OpenOrdersSlim { @@ -61,9 +64,15 @@ impl OpenOrdersAmounts for OpenOrdersSlim { fn native_base_total(&self) -> u64 { self.native_coin_total } + fn native_quote_total(&self) -> u64 { + self.native_pc_total + } fn native_quote_total_plus_rebates(&self) -> u64 { cm!(self.native_pc_total + self.referrer_rebates_accrued) } + fn native_rebates(&self) -> u64 { + self.referrer_rebates_accrued + } } impl OpenOrdersAmounts for OpenOrders { @@ -85,9 +94,15 @@ impl OpenOrdersAmounts for OpenOrders { fn native_base_total(&self) -> u64 { self.native_coin_total } + fn native_quote_total(&self) -> u64 { + self.native_pc_total + } fn native_quote_total_plus_rebates(&self) -> u64 { cm!(self.native_pc_total + self.referrer_rebates_accrued) } + fn native_rebates(&self) -> u64 { + self.referrer_rebates_accrued + } } /// Copy paste a bunch of enums so that we could AnchorSerialize & AnchorDeserialize them @@ -298,6 +313,19 @@ pub fn serum3_place_order( let oo_ai = &ctx.accounts.open_orders.as_ref(); let open_orders = load_open_orders_ref(oo_ai)?; let after_oo = OpenOrdersSlim::from_oo(&open_orders); + + emit!(Serum3OpenOrdersBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + base_token_index: serum_market.base_token_index, + quote_token_index: serum_market.quote_token_index, + base_total: after_oo.native_coin_total, + base_free: after_oo.native_coin_free, + quote_total: after_oo.native_pc_total, + quote_free: after_oo.native_pc_free, + referrer_rebates_accrued: after_oo.referrer_rebates_accrued, + }); + OODifference::new(&before_oo, &after_oo) }; @@ -314,6 +342,7 @@ pub fn serum3_place_order( let vault_difference = { let mut payer_bank = ctx.accounts.payer_bank.load_mut()?; apply_vault_difference( + ctx.accounts.account.key(), &mut account.borrow_mut(), serum_market.market_index, &mut payer_bank, @@ -386,7 +415,9 @@ impl VaultDifference { /// Called in settle_funds, place_order, liq_force_cancel to adjust token positions after /// changing the vault balances +/// Also logs changes to token balances pub fn apply_vault_difference( + account_pk: Pubkey, account: &mut MangoAccountRefMut, serum_market_index: Serum3MarketIndex, bank: &mut Bank, @@ -406,6 +437,7 @@ pub fn apply_vault_difference( .abs() .to_num::(); + let indexed_position = position.indexed_position; let market = account.serum3_orders_mut(serum_market_index).unwrap(); let borrows_without_fee = if bank.token_index == market.base_token_index { &mut market.base_borrows_without_fee @@ -426,6 +458,15 @@ pub fn apply_vault_difference( *borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::()); } + emit!(TokenBalanceLog { + mango_group: bank.group, + mango_account: account_pk, + token_index: bank.token_index, + indexed_position: indexed_position.to_bits(), + deposit_index: bank.deposit_index.to_bits(), + borrow_index: bank.borrow_index.to_bits(), + }); + Ok(VaultDifference { token_index: bank.token_index, native_change, diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index 6b87ac63b..d82865a8b 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -8,6 +8,7 @@ use crate::serum3_cpi::load_open_orders_ref; use crate::state::*; use super::{apply_vault_difference, OpenOrdersAmounts, OpenOrdersSlim}; +use crate::logs::Serum3OpenOrdersBalanceLog; use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog}; #[derive(Accounts)] @@ -158,6 +159,7 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; apply_vault_difference( + ctx.accounts.account.key(), &mut account.borrow_mut(), serum_market.market_index, &mut base_bank, @@ -165,6 +167,7 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { before_base_vault, )?; apply_vault_difference( + ctx.accounts.account.key(), &mut account.borrow_mut(), serum_market.market_index, &mut quote_bank, @@ -173,6 +176,21 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { )?; } + let oo_ai = &ctx.accounts.open_orders.as_ref(); + let open_orders = load_open_orders_ref(oo_ai)?; + let after_oo = OpenOrdersSlim::from_oo(&open_orders); + emit!(Serum3OpenOrdersBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + base_token_index: serum_market.base_token_index, + quote_token_index: serum_market.quote_token_index, + base_total: after_oo.native_base_total(), + base_free: after_oo.native_base_free(), + quote_total: after_oo.native_quote_total(), + quote_free: after_oo.native_quote_free(), + referrer_rebates_accrued: after_oo.native_rebates(), + }); + Ok(()) } diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index dce2428f5..c0fab0c14 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -88,7 +88,6 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { indexed_position: indexed_position.to_bits(), deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), - price: oracle_price.to_bits(), }); // diff --git a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs index 9653d552d..d904428a1 100644 --- a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs @@ -158,7 +158,7 @@ pub fn token_liq_bankruptcy( )?; // move quote assets into liqor and withdraw liab assets - if let Some((quote_bank, quote_price)) = opt_quote_bank_and_price { + if let Some((quote_bank, _)) = opt_quote_bank_and_price { // account constraint #2 a) require_keys_eq!(quote_bank.vault, ctx.accounts.quote_vault.key()); require_keys_eq!(quote_bank.mint, ctx.accounts.insurance_vault.mint); @@ -193,7 +193,6 @@ pub fn token_liq_bankruptcy( indexed_position: liqor_quote_indexed_position.to_bits(), deposit_index: quote_deposit_index.to_bits(), borrow_index: quote_borrow_index.to_bits(), - price: quote_price.to_bits(), }); if loan_origination_fee.is_positive() { @@ -274,7 +273,6 @@ pub fn token_liq_bankruptcy( indexed_position: liqee_liab.indexed_position.to_bits(), deposit_index: liab_deposit_index.to_bits(), borrow_index: liab_borrow_index.to_bits(), - price: liab_price.to_bits(), }); // liqee liab @@ -285,7 +283,6 @@ pub fn token_liq_bankruptcy( indexed_position: liqee_liab.indexed_position.to_bits(), deposit_index: liab_deposit_index.to_bits(), borrow_index: liab_borrow_index.to_bits(), - price: liab_price.to_bits(), }); let liab_bank = bank_ais[0].load::()?; diff --git a/programs/mango-v4/src/instructions/token_liq_with_token.rs b/programs/mango-v4/src/instructions/token_liq_with_token.rs index eb5cefd30..76fae6b82 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -187,7 +187,6 @@ pub fn token_liq_with_token( indexed_position: liqee_asset_position_indexed.to_bits(), deposit_index: asset_bank.deposit_index.to_bits(), borrow_index: asset_bank.borrow_index.to_bits(), - price: asset_price.to_bits(), }); // liqee liab emit!(TokenBalanceLog { @@ -197,7 +196,6 @@ pub fn token_liq_with_token( indexed_position: liqee_liab_position_indexed.to_bits(), deposit_index: liab_bank.deposit_index.to_bits(), borrow_index: liab_bank.borrow_index.to_bits(), - price: liab_price.to_bits(), }); // liqor asset emit!(TokenBalanceLog { @@ -207,7 +205,6 @@ pub fn token_liq_with_token( indexed_position: liqor_asset_position_indexed.to_bits(), deposit_index: asset_bank.deposit_index.to_bits(), borrow_index: asset_bank.borrow_index.to_bits(), - price: asset_price.to_bits(), }); // liqor liab emit!(TokenBalanceLog { @@ -217,7 +214,6 @@ pub fn token_liq_with_token( indexed_position: liqor_liab_position_indexed.to_bits(), deposit_index: liab_bank.deposit_index.to_bits(), borrow_index: liab_bank.borrow_index.to_bits(), - price: liab_price.to_bits(), }); if loan_origination_fee.is_positive() { diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 71241f6d3..d2063ed6f 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -131,7 +131,6 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo indexed_position: position.indexed_position.to_bits(), deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), - price: oracle_price.to_bits(), }); // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index 366651eda..ef2be05be 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -10,7 +10,6 @@ pub fn emit_perp_balances( mango_group: Pubkey, mango_account: Pubkey, market_index: u64, - price: i64, pp: &PerpPosition, pm: &PerpMarket, ) { @@ -22,7 +21,6 @@ pub fn emit_perp_balances( quote_position: pp.quote_position_native().to_bits(), long_settled_funding: pp.long_settled_funding.to_bits(), short_settled_funding: pp.short_settled_funding.to_bits(), - price, long_funding: pm.long_funding.to_bits(), short_funding: pm.short_funding.to_bits(), }); @@ -37,9 +35,8 @@ pub struct PerpBalanceLog { pub quote_position: i128, // I80F48 pub long_settled_funding: i128, // I80F48 pub short_settled_funding: i128, // I80F48 - pub price: i64, - pub long_funding: i128, // I80F48 - pub short_funding: i128, // I80F48 + pub long_funding: i128, // I80F48 + pub short_funding: i128, // I80F48 } #[event] @@ -50,7 +47,6 @@ pub struct TokenBalanceLog { pub indexed_position: i128, // on client convert i128 to I80F48 easily by passing in the BN to I80F48 ctor pub deposit_index: i128, // I80F48 pub borrow_index: i128, // I80F48 - pub price: i128, // I80F48 } #[derive(AnchorSerialize, AnchorDeserialize)] @@ -165,17 +161,17 @@ pub struct LiquidateTokenAndTokenLog { } #[event] -pub struct OpenOrdersBalanceLog { +pub struct Serum3OpenOrdersBalanceLog { pub mango_group: Pubkey, pub mango_account: Pubkey, - pub market_index: u16, + pub base_token_index: u16, + pub quote_token_index: u16, pub base_total: u64, pub base_free: u64, /// this field does not include the referrer_rebates; need to add that in to get true total pub quote_total: u64, pub quote_free: u64, pub referrer_rebates_accrued: u64, - pub price: i128, // I80F48 } #[derive(PartialEq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)] diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 4d1c4ab1d..f4f4bb827 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -5425,11 +5425,6 @@ export type MangoV4 = { "type": "i128", "index": false }, - { - "name": "price", - "type": "i64", - "index": false - }, { "name": "longFunding", "type": "i128", @@ -5474,11 +5469,6 @@ export type MangoV4 = { "name": "borrowIndex", "type": "i128", "index": false - }, - { - "name": "price", - "type": "i128", - "index": false } ] }, @@ -5844,7 +5834,7 @@ export type MangoV4 = { ] }, { - "name": "OpenOrdersBalanceLog", + "name": "Serum3OpenOrdersBalanceLog", "fields": [ { "name": "mangoGroup", @@ -5857,7 +5847,12 @@ export type MangoV4 = { "index": false }, { - "name": "marketIndex", + "name": "baseTokenIndex", + "type": "u16", + "index": false + }, + { + "name": "quoteTokenIndex", "type": "u16", "index": false }, @@ -5885,11 +5880,6 @@ export type MangoV4 = { "name": "referrerRebatesAccrued", "type": "u64", "index": false - }, - { - "name": "price", - "type": "i128", - "index": false } ] }, @@ -11522,11 +11512,6 @@ export const IDL: MangoV4 = { "type": "i128", "index": false }, - { - "name": "price", - "type": "i64", - "index": false - }, { "name": "longFunding", "type": "i128", @@ -11571,11 +11556,6 @@ export const IDL: MangoV4 = { "name": "borrowIndex", "type": "i128", "index": false - }, - { - "name": "price", - "type": "i128", - "index": false } ] }, @@ -11941,7 +11921,7 @@ export const IDL: MangoV4 = { ] }, { - "name": "OpenOrdersBalanceLog", + "name": "Serum3OpenOrdersBalanceLog", "fields": [ { "name": "mangoGroup", @@ -11954,7 +11934,12 @@ export const IDL: MangoV4 = { "index": false }, { - "name": "marketIndex", + "name": "baseTokenIndex", + "type": "u16", + "index": false + }, + { + "name": "quoteTokenIndex", "type": "u16", "index": false }, @@ -11982,11 +11967,6 @@ export const IDL: MangoV4 = { "name": "referrerRebatesAccrued", "type": "u64", "index": false - }, - { - "name": "price", - "type": "i128", - "index": false } ] }, From e509a3ae9c368bafeff375df7125029b8f4770ff Mon Sep 17 00:00:00 2001 From: Nicholas Clarke Date: Fri, 23 Sep 2022 11:39:51 -0700 Subject: [PATCH 13/39] Clarkeni/overall pnl (#232) * Add getPNL client function --- ts/client/src/accounts/mangoAccount.ts | 11 +++++++++++ ts/client/src/client.ts | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 5d53bd19e..5fe16409e 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -21,6 +21,7 @@ export class MangoAccount { public perps: PerpPosition[]; public perpOpenOrders: PerpOo[]; public name: string; + public netDeposits: BN; static from( publicKey: PublicKey, @@ -85,6 +86,7 @@ export class MangoAccount { this.perps = perps.map((dto) => PerpPosition.from(dto)); this.perpOpenOrders = perpOpenOrders.map((dto) => PerpOo.from(dto)); this.accountData = undefined; + this.netDeposits = netDeposits; } async reload(client: MangoClient, group: Group): Promise { @@ -279,6 +281,15 @@ export class MangoAccount { return this.accountData?.healthCache.liabs(healthType); } + /** + * @returns Overall PNL, in native quote + * PNL is defined here as spot value + serum3 open orders value + perp value - net deposits value (evaluated at native quote price at the time of the deposit/withdraw) + * spot value + serum3 open orders value + perp value is returned by getEquity (open orders values are added to spot token values implicitly) + */ + getPnl(): I80F48 | undefined { + return this.getEquity()?.add((I80F48.fromI64(this.netDeposits)).mul(I80F48.fromNumber(-1))) + } + /** * The amount of given native token you can withdraw including borrows, considering all existing assets as collateral. * @returns amount of given native token you can borrow, considering all existing assets as collateral, in native token diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 3d91b7cc9..3110f6791 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -610,6 +610,13 @@ export class MangoClient { ); } + public async getMangoAccountForPublicKey(mangoAccountPk: PublicKey) { + return MangoAccount.from( + mangoAccountPk, + await this.program.account.mangoAccount.fetch(mangoAccountPk), + ); + } + public async getMangoAccountWithSlot(mangoAccountPk: PublicKey) { const resp = await this.program.provider.connection.getAccountInfoAndContext( From b7aa6dbd5dca9edf5a717d8aa7c253606648c150 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Sat, 24 Sep 2022 07:11:57 +0200 Subject: [PATCH 14/39] ts: tiny liabs pushes health ratio beyond what I80F48 can support (#243) * ts: tiny liabs pushes health ratio beyond what I80F48 can support Signed-off-by: microwavedcola1 * add alias Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- package.json | 1 + ts/client/src/accounts/healthCache.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a6a92dff5..9e46e2836 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "build": "npm run build:esm; npm run build:cjs", "build:cjs": "tsc -p tsconfig.cjs.json", "build:esm": "tsc -p tsconfig.esm.json", + "test": "ts-mocha ts/client/**/*.spec.ts", "clean": "rm -rf dist", "example1-user": "ts-node ts/client/src/scripts/example1-user.ts", "example1-admin": "ts-node ts/client/src/scripts/example1-admin.ts", diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 703a987c4..6762ab45e 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -173,7 +173,7 @@ export class HealthCache { } } - if (liabs.isPos()) { + if (liabs.gt(I80F48.fromNumber(0.001))) { return HUNDRED_I80F48().mul(assets.sub(liabs).div(liabs)); } else { return MAX_I80F48(); From b57e50c75b12a1042cd0c9553c32777380214152 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 27 Sep 2022 13:59:00 +0200 Subject: [PATCH 15/39] ts: fix tsc issues Signed-off-by: microwavedcola1 --- ts/client/src/accounts/bank.ts | 44 +++++++++++++++------- ts/client/src/accounts/group.ts | 8 ++-- ts/client/src/accounts/healthCache.spec.ts | 32 ++++++++-------- 3 files changed, 50 insertions(+), 34 deletions(-) diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index 7fe584b55..e5a8d8668 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -10,18 +10,16 @@ export type OracleConfig = { confFilter: I80F48Dto; }; -export class BankForHealth { - constructor( - public tokenIndex: number, - public maintAssetWeight: I80F48, - public initAssetWeight: I80F48, - public maintLiabWeight: I80F48, - public initLiabWeight: I80F48, - public price: I80F48 | undefined, - ) {} +export interface BankForHealth { + tokenIndex: number; + maintAssetWeight: I80F48; + initAssetWeight: I80F48; + maintLiabWeight: I80F48; + initLiabWeight: I80F48; + price: I80F48; } -export class Bank extends BankForHealth { +export class Bank implements BankForHealth { public name: string; public depositIndex: I80F48; public borrowIndex: I80F48; @@ -36,8 +34,8 @@ export class Bank extends BankForHealth { public rate1: I80F48; public util0: I80F48; public util1: I80F48; - public price: I80F48 | undefined; - public uiPrice: number | undefined; + public _price: I80F48 | undefined; + public _uiPrice: number | undefined; public collectedFeesNative: I80F48; public loanFeeRate: I80F48; public loanOriginationFeeRate: I80F48; @@ -189,8 +187,8 @@ export class Bank extends BankForHealth { this.initLiabWeight = I80F48.from(initLiabWeight); this.liquidationFee = I80F48.from(liquidationFee); this.dust = I80F48.from(dust); - this.price = undefined; - this.uiPrice = undefined; + this._price = undefined; + this._uiPrice = undefined; } toString(): string { @@ -267,6 +265,24 @@ export class Bank extends BankForHealth { ); } + get price(): I80F48 { + if (!this._price) { + throw new Error( + `Undefined price for bank ${this.publicKey}, tokenIndex ${this.tokenIndex}`, + ); + } + return this._price; + } + + get uiPrice(): number { + if (!this._uiPrice) { + throw new Error( + `Undefined uiPrice for bank ${this.publicKey}, tokenIndex ${this.tokenIndex}`, + ); + } + return this._uiPrice; + } + nativeDeposits(): I80F48 { return this.indexedDeposits.mul(this.depositIndex); } diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 4c981de96..f78064c31 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -244,8 +244,8 @@ export class Group { for (const [index, ai] of ais.entries()) { for (const bank of banks[index]) { if (bank.name === 'USDC') { - bank.price = ONE_I80F48(); - bank.uiPrice = 1; + bank._price = ONE_I80F48(); + bank._uiPrice = 1; } else { if (!ai) throw new Error( @@ -257,8 +257,8 @@ export class Group { ai, this.getMintDecimals(bank.mint), ); - bank.price = price; - bank.uiPrice = uiPrice; + bank._price = price; + bank._uiPrice = uiPrice; } } } diff --git a/ts/client/src/accounts/healthCache.spec.ts b/ts/client/src/accounts/healthCache.spec.ts index c70732a37..5d1697083 100644 --- a/ts/client/src/accounts/healthCache.spec.ts +++ b/ts/client/src/accounts/healthCache.spec.ts @@ -7,23 +7,23 @@ import { I80F48, ZERO_I80F48 } from './I80F48'; describe('Health Cache', () => { it('max swap tokens for min ratio', () => { // USDC like - const sourceBank = new BankForHealth( - 0, - I80F48.fromNumber(1), - I80F48.fromNumber(1), - I80F48.fromNumber(1), - I80F48.fromNumber(1), - I80F48.fromNumber(1), - ); + const sourceBank: BankForHealth = { + tokenIndex: 0, + maintAssetWeight: I80F48.fromNumber(1), + initAssetWeight: I80F48.fromNumber(1), + maintLiabWeight: I80F48.fromNumber(1), + initLiabWeight: I80F48.fromNumber(1), + price: I80F48.fromNumber(1), + }; // BTC like - const targetBank = new BankForHealth( - 1, - I80F48.fromNumber(0.9), - I80F48.fromNumber(0.8), - I80F48.fromNumber(1.1), - I80F48.fromNumber(1.2), - I80F48.fromNumber(20000), - ); + const targetBank: BankForHealth = { + tokenIndex: 1, + maintAssetWeight: I80F48.fromNumber(0.9), + initAssetWeight: I80F48.fromNumber(0.8), + maintLiabWeight: I80F48.fromNumber(1.1), + initLiabWeight: I80F48.fromNumber(1.2), + price: I80F48.fromNumber(20000), + }; const hc = new HealthCache( [ From 8d86883190106d1c0ced6241140c251786404ebc Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 27 Sep 2022 17:33:51 +0200 Subject: [PATCH 16/39] ts: remove debug code Signed-off-by: microwavedcola1 --- ts/client/src/accounts/healthCache.ts | 18 +----------------- ts/client/src/accounts/mangoAccount.ts | 6 ++++-- ts/client/src/client.ts | 2 +- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 6762ab45e..c7444cfed 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -46,22 +46,6 @@ export class HealthCache { ) {} static fromDto(dto) { - // console.log( - JSON.stringify( - dto, - function replacer(k, v) { - // console.log(k); - console.log(v); - // if (v instanceof BN) { - // console.log(v); - // return new I80F48(v).toNumber(); - // } - // return v; - }, - 2, - ), - // ); - process.exit(0); return new HealthCache( dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)), dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)), @@ -173,7 +157,7 @@ export class HealthCache { } } - if (liabs.gt(I80F48.fromNumber(0.001))) { + if (liabs.gt(I80F48.fromNumber(0.001))) { return HUNDRED_I80F48().mul(assets.sub(liabs).div(liabs)); } else { return MAX_I80F48(); diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 5fe16409e..d97166bb5 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -286,8 +286,10 @@ export class MangoAccount { * PNL is defined here as spot value + serum3 open orders value + perp value - net deposits value (evaluated at native quote price at the time of the deposit/withdraw) * spot value + serum3 open orders value + perp value is returned by getEquity (open orders values are added to spot token values implicitly) */ - getPnl(): I80F48 | undefined { - return this.getEquity()?.add((I80F48.fromI64(this.netDeposits)).mul(I80F48.fromNumber(-1))) + getPnl(): I80F48 | undefined { + return this.getEquity()?.add( + I80F48.fromI64(this.netDeposits).mul(I80F48.fromNumber(-1)), + ); } /** diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 3110f6791..316220daa 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -616,7 +616,7 @@ export class MangoClient { await this.program.account.mangoAccount.fetch(mangoAccountPk), ); } - + public async getMangoAccountWithSlot(mangoAccountPk: PublicKey) { const resp = await this.program.provider.connection.getAccountInfoAndContext( From 9cbc352197f890fef1501e36490b09fa009e432a Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 15 Sep 2022 09:57:48 +0200 Subject: [PATCH 17/39] liquidator: force-cancel perp orders, liq perp base positions --- client/src/client.rs | 96 +++++++++++- liquidator/src/liquidate.rs | 95 +++++++++++- liquidator/src/main.rs | 3 + ts/client/src/client.ts | 133 ++++++++++++++++- .../src/scripts/mb-example1-admin-close.ts | 18 +-- .../src/scripts/mb-liqtest-create-group.ts | 52 ++++++- .../src/scripts/mb-liqtest-deposit-tokens.ts | 19 ++- .../src/scripts/mb-liqtest-make-candidates.ts | 138 ++++++++++++++++-- .../mb-liqtest-settle-and-close-all.ts | 60 +------- 9 files changed, 512 insertions(+), 102 deletions(-) diff --git a/client/src/client.rs b/client/src/client.rs index 6f9f56b15..0170f7bed 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -15,14 +15,16 @@ use bincode::Options; use fixed::types::I80F48; use itertools::Itertools; use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; -use mango_v4::state::{Bank, Group, MangoAccountValue, Serum3MarketIndex, TokenIndex}; +use mango_v4::state::{ + Bank, Group, MangoAccountValue, PerpMarketIndex, Serum3MarketIndex, TokenIndex, +}; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_client::rpc_client::RpcClient; use solana_sdk::signer::keypair; use crate::account_fetcher::*; -use crate::context::{MangoGroupContext, Serum3MarketContext, TokenContext}; +use crate::context::{MangoGroupContext, PerpMarketContext, Serum3MarketContext, TokenContext}; use crate::gpa::fetch_mango_accounts; use crate::jupiter; use crate::util::MyClone; @@ -836,12 +838,98 @@ impl MangoClient { // // Perps // + fn perp_data_by_market_index( + &self, + market_index: PerpMarketIndex, + ) -> Result<&PerpMarketContext, ClientError> { + Ok(self.context.perp_markets.get(&market_index).unwrap()) + } + + pub fn perp_liq_force_cancel_orders( + &self, + liqee: (&Pubkey, &MangoAccountValue), + market_index: PerpMarketIndex, + ) -> anyhow::Result { + let perp = self.perp_data_by_market_index(market_index)?; + + let health_remaining_ams = self + .context + .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, + asks: perp.market.asks, + bids: perp.market.bids, + oracle: perp.market.oracle, + }, + 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) + } + + pub fn perp_liq_base_position( + &self, + liqee: (&Pubkey, &MangoAccountValue), + market_index: PerpMarketIndex, + max_base_transfer: i64, + ) -> anyhow::Result { + let perp = self.perp_data_by_market_index(market_index)?; + + let health_remaining_ams = self + .context + .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::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) + } // // Liquidation // - pub fn liq_token_with_token( + pub fn token_liq_with_token( &self, liqee: (&Pubkey, &MangoAccountValue), asset_token_index: TokenIndex, @@ -886,7 +974,7 @@ impl MangoClient { .map_err(prettify_client_error) } - pub fn liq_token_bankruptcy( + pub fn token_liq_bankruptcy( &self, liqee: (&Pubkey, &MangoAccountValue), liab_token_index: TokenIndex, diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 3986a830e..1572d2614 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -5,7 +5,7 @@ use crate::account_shared_data::KeyedAccountSharedData; use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext}; use mango_v4::state::{ new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue, - Serum3Orders, TokenIndex, QUOTE_TOKEN_INDEX, + PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX, }; use itertools::Itertools; @@ -177,6 +177,36 @@ pub fn maybe_liquidate_account( .filter_map_ok(|v| v) .collect::>>()?; + // look for any perp open orders and base positions + let perp_force_cancels = account + .active_perp_positions() + .filter_map(|pp| pp.has_open_orders().then(|| pp.market_index)) + .collect::>(); + let mut perp_base_positions = account + .active_perp_positions() + .map(|pp| { + let base_lots = pp.base_position_lots(); + if base_lots == 0 { + return Ok(None); + } + let perp = mango_client.context.perp(pp.market_index); + let oracle = account_fetcher.fetch_raw_account(perp.market.oracle)?; + let price = perp.market.oracle_price(&KeyedAccountSharedData::new( + perp.market.oracle, + oracle.into(), + ))?; + Ok(Some(( + pp.market_index, + base_lots, + price, + I80F48::from(base_lots.abs()) * price, + ))) + }) + .filter_map_ok(|v| v) + .collect::>>()?; + // sort by base_position_value, ascending + perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3)); + let get_max_liab_transfer = |source, target| -> anyhow::Result { let mut liqor = account_fetcher .fetch_fresh_mango_account(&mango_client.mango_account_address) @@ -203,7 +233,7 @@ pub fn maybe_liquidate_account( // try liquidating let txsig = if !serum_force_cancels.is_empty() { - // pick a random market to force-cancel orders on + // Cancel all orders on a random serum market let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap(); let sig = mango_client.serum3_liq_force_cancel_orders( (pubkey, &account), @@ -211,13 +241,68 @@ pub fn maybe_liquidate_account( &serum_orders.open_orders, )?; log::info!( - "Force cancelled serum market on account {}, market index {}, maint_health was {}, tx sig {:?}", + "Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}", pubkey, serum_orders.market_index, maint_health, sig ); sig + } else if !perp_force_cancels.is_empty() { + // Cancel all orders on a random perp market + let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap(); + let sig = + mango_client.perp_liq_force_cancel_orders((pubkey, &account), perp_market_index)?; + log::info!( + "Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}", + pubkey, + perp_market_index, + maint_health, + sig + ); + sig + } else if !perp_base_positions.is_empty() { + // Liquidate the highest-value perp base position + let (perp_market_index, base_lots, price, _) = perp_base_positions.last().unwrap(); + let perp = mango_client.context.perp(*perp_market_index); + + let (side, side_signum) = if *base_lots > 0 { + (Side::Bid, 1) + } else { + (Side::Ask, -1) + }; + + // Compute the max number of base_lots the liqor is willing to take + let max_base_transfer_abs = { + let mut liqor = account_fetcher + .fetch_fresh_mango_account(&mango_client.mango_account_address) + .context("getting liquidator account")?; + liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?; + let health_cache = new_health_cache_(&mango_client.context, account_fetcher, &liqor) + .expect("always ok"); + health_cache.max_perp_for_health_ratio( + *perp_market_index, + *price, + perp.market.base_lot_size, + side, + min_health_ratio, + )? + }; + log::info!("computed max_base_transfer to be {max_base_transfer_abs}"); + + let sig = mango_client.perp_liq_base_position( + (pubkey, &account), + *perp_market_index, + side_signum * max_base_transfer_abs, + )?; + log::info!( + "Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}", + pubkey, + perp_market_index, + maint_health, + sig + ); + sig } else if is_spot_bankrupt { if tokens.is_empty() { anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey); @@ -240,7 +325,7 @@ pub fn maybe_liquidate_account( let max_liab_transfer = get_max_liab_transfer(liab_token_index, quote_token_index)?; let sig = mango_client - .liq_token_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer) + .token_liq_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer) .context("sending liq_token_bankruptcy")?; log::info!( "Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}", @@ -288,7 +373,7 @@ pub fn maybe_liquidate_account( // TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side // let sig = mango_client - .liq_token_with_token( + .token_liq_with_token( (pubkey, &account), asset_token_index, liab_token_index, diff --git a/liquidator/src/main.rs b/liquidator/src/main.rs index c8aa90fc4..faf84ce1c 100644 --- a/liquidator/src/main.rs +++ b/liquidator/src/main.rs @@ -8,6 +8,7 @@ use client::{chain_data, keypair_from_cli, Client, MangoClient, MangoGroupContex use log::*; use mango_v4::state::{PerpMarketIndex, TokenIndex}; +use itertools::Itertools; use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::pubkey::Pubkey; use std::collections::HashSet; @@ -126,6 +127,8 @@ async fn main() -> anyhow::Result<()> { .tokens .values() .map(|value| value.mint_info.oracle) + .chain(group_context.perp_markets.values().map(|p| p.market.oracle)) + .unique() .collect::>(); // diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index b091cc613..a881ce8e5 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -34,7 +34,14 @@ import { PerpPosition, } from './accounts/mangoAccount'; import { StubOracle } from './accounts/oracle'; -import { PerpMarket, PerpOrderSide, PerpOrderType } from './accounts/perp'; +import { + PerpEventQueue, + PerpMarket, + PerpOrderType, + PerpOrderSide, + FillEvent, + OutEvent, +} from './accounts/perp'; import { generateSerum3MarketExternalVaultSignerAddress, Serum3Market, @@ -541,9 +548,20 @@ export class MangoClient { group: Group, accountNumber?: number, name?: string, + tokenCount?: number, + serum3Count?: number, + perpCount?: number, + perpOoCount?: number, ): Promise { const transaction = await this.program.methods - .accountCreate(accountNumber ?? 0, 8, 8, 0, 0, name ?? '') + .accountCreate( + accountNumber ?? 0, + tokenCount ?? 8, + serum3Count ?? 8, + perpCount ?? 0, + perpOoCount ?? 0, + name ?? '', + ) .accounts({ group: group.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, @@ -560,6 +578,32 @@ export class MangoClient { ); } + public async createAndFetchMangoAccount( + group: Group, + accountNumber?: number, + name?: string, + tokenCount?: number, + serum3Count?: number, + perpCount?: number, + perpOoCount?: number, + ): Promise { + const accNum = accountNumber ?? 0; + await this.createMangoAccount( + group, + accNum, + name, + tokenCount, + serum3Count, + perpCount, + perpOoCount, + ); + return await this.getMangoAccountForOwner( + group, + (this.program.provider as AnchorProvider).wallet.publicKey, + accNum, + ); + } + public async expandMangoAccount( group: Group, account: MangoAccount, @@ -1511,6 +1555,37 @@ export class MangoClient { ); } + async perpDeactivatePosition( + group: Group, + mangoAccount: MangoAccount, + perpMarketName: string, + ): Promise { + const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const healthRemainingAccounts: PublicKey[] = + this.buildHealthRemainingAccounts( + AccountRetriever.Fixed, + group, + [mangoAccount], + [], + [], + ); + return await this.program.methods + .perpDeactivatePosition() + .accounts({ + group: group.publicKey, + account: mangoAccount.publicKey, + perpMarket: perpMarket.publicKey, + owner: (this.program.provider as AnchorProvider).wallet.publicKey, + }) + .remainingAccounts( + healthRemainingAccounts.map( + (pk) => + ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), + ), + ) + .rpc(); + } + async perpPlaceOrder( group: Group, mangoAccount: MangoAccount, @@ -1586,6 +1661,60 @@ export class MangoClient { .rpc(); } + async perpConsumeEvents( + group: Group, + perpMarketName: string, + accounts: PublicKey[], + limit: number, + ): Promise { + const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + return await this.program.methods + .perpConsumeEvents(new BN(limit)) + .accounts({ + group: group.publicKey, + perpMarket: perpMarket.publicKey, + eventQueue: perpMarket.eventQueue, + }) + .remainingAccounts( + accounts.map( + (pk) => + ({ pubkey: pk, isWritable: true, isSigner: false } as AccountMeta), + ), + ) + .rpc(); + } + + async perpConsumeAllEvents( + group: Group, + perpMarketName: string, + ): Promise { + const limit = 8; + const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const eventQueue = await perpMarket.loadEventQueue(this); + let unconsumedEvents = eventQueue.getUnconsumedEvents(); + while (unconsumedEvents.length > 0) { + const events = unconsumedEvents.splice(0, limit); + const accounts = events + .map((ev) => { + switch (ev.eventType) { + case PerpEventQueue.FILL_EVENT_TYPE: + const fill = ev; + return [fill.maker, fill.taker]; + case PerpEventQueue.OUT_EVENT_TYPE: + const out = ev; + return [out.owner]; + case PerpEventQueue.LIQUIDATE_EVENT_TYPE: + return []; + default: + throw new Error(`Unknown event with eventType ${ev.eventType}`); + } + }) + .flat(); + + await this.perpConsumeEvents(group, perpMarketName, accounts, limit); + } + } + public async marginTrade({ group, mangoAccount, diff --git a/ts/client/src/scripts/mb-example1-admin-close.ts b/ts/client/src/scripts/mb-example1-admin-close.ts index e17b95bef..90ee75f5b 100644 --- a/ts/client/src/scripts/mb-example1-admin-close.ts +++ b/ts/client/src/scripts/mb-example1-admin-close.ts @@ -45,15 +45,6 @@ async function main() { let sig; - // close stub oracles - const stubOracles = await client.getStubOracle(group); - for (const stubOracle of stubOracles) { - sig = await client.stubOracleClose(group, stubOracle.publicKey); - console.log( - `Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`, - ); - } - // close all banks for (const banks of group.banksMapByMint.values()) { sig = await client.tokenDeregister(group, banks[0].mint); @@ -81,6 +72,15 @@ async function main() { ); } + // close stub oracles + const stubOracles = await client.getStubOracle(group); + for (const stubOracle of stubOracles) { + sig = await client.stubOracleClose(group, stubOracle.publicKey); + console.log( + `Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`, + ); + } + // finally, close the group sig = await client.groupClose(group); console.log(`Closed group, 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 9963ae067..e464cd6eb 100644 --- a/ts/client/src/scripts/mb-liqtest-create-group.ts +++ b/ts/client/src/scripts/mb-liqtest-create-group.ts @@ -16,12 +16,14 @@ const MAINNET_MINTS = new Map([ ['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], ['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], ['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 ]); // External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json @@ -179,14 +181,48 @@ async function main() { } console.log('Registering SOL/USDC serum market...'); - await client.serum3RegisterMarket( - group, - new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!), - group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)), - group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)), - 1, - 'SOL/USDC', - ); + try { + await client.serum3RegisterMarket( + group, + new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!), + group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)), + group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)), + 1, + 'SOL/USDC', + ); + } catch (error) { + console.log(error); + } + + console.log('Registering MNGO-PERP market...'); + const mngoMainnetOracle = oracles.get('MNGO'); + try { + await client.perpCreateMarket( + group, + mngoMainnetOracle, + 0, + 'MNGO-PERP', + 0.1, + 9, + 0, + 10, + 100000, // base lots + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + -0.001, + 0.002, + -0.1, + 0.1, + 10, + false, + false, + ); + } catch (error) { + console.log(error); + } process.exit(); } diff --git a/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts b/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts index 5eeaa6555..c351824a4 100644 --- a/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts +++ b/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts @@ -39,11 +39,14 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); - const mangoAccount = (await client.getOrCreateMangoAccount( + const mangoAccount = (await client.createAndFetchMangoAccount( group, - admin.publicKey, ACCOUNT_NUM, 'LIQTEST, FUNDING', + 8, + 4, + 4, + 4, ))!; console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(mangoAccount.toString()); @@ -54,16 +57,16 @@ async function main() { // deposit try { - console.log(`...depositing 10 USDC`); - await client.tokenDeposit(group, mangoAccount, usdcMint, 10); + console.log(`...depositing 5 USDC`); + await client.tokenDeposit(group, mangoAccount, usdcMint, 5); await mangoAccount.reload(client, group); - console.log(`...depositing 0.0004 BTC`); - await client.tokenDeposit(group, mangoAccount, btcMint, 0.0004); + console.log(`...depositing 0.0002 BTC`); + await client.tokenDeposit(group, mangoAccount, btcMint, 0.0002); await mangoAccount.reload(client, group); - console.log(`...depositing 0.25 SOL`); - await client.tokenDeposit(group, mangoAccount, solMint, 0.25); + console.log(`...depositing 0.15 SOL`); + await client.tokenDeposit(group, mangoAccount, solMint, 0.15); await mangoAccount.reload(client, group); } catch (error) { console.log(error); diff --git a/ts/client/src/scripts/mb-liqtest-make-candidates.ts b/ts/client/src/scripts/mb-liqtest-make-candidates.ts index 2a698c6ec..f05febe3b 100644 --- a/ts/client/src/scripts/mb-liqtest-make-candidates.ts +++ b/ts/client/src/scripts/mb-liqtest-make-candidates.ts @@ -6,6 +6,8 @@ import { Serum3SelfTradeBehavior, Serum3Side, } from '../accounts/serum3'; +import { Side, PerpOrderType } from '../accounts/perp'; +import { MangoAccount } from '../accounts/mangoAccount'; import { MangoClient } from '../client'; import { MANGO_V4_ID } from '../constants'; @@ -26,6 +28,7 @@ const MAINNET_MINTS = new Map([ ['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], ['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], ['SOL', 'So11111111111111111111111111111111111111112'], + ['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'], ]); const TOKEN_SCENARIOS: [string, string, number, string, number][] = [ @@ -66,19 +69,30 @@ async function main() { 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; + maxAccountNum = maxAccountNum + 1; + await client.createMangoAccount(group, accountNum, name, 4, 4, 4, 4); + return (await client.getMangoAccountForOwner( + group, + admin.publicKey, + accountNum, + ))!; + } for (const scenario of TOKEN_SCENARIOS) { const [name, assetName, assetAmount, liabName, liabAmount] = scenario; // create account console.log(`Creating mangoaccount...`); - let mangoAccount = (await client.getOrCreateMangoAccount( - group, - admin.publicKey, - maxAccountNum + 1, - name, - ))!; - maxAccountNum = maxAccountNum + 1; + let mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); @@ -119,13 +133,7 @@ async function main() { const name = 'LIQTEST, serum orders'; console.log(`Creating mangoaccount...`); - let mangoAccount = (await client.getOrCreateMangoAccount( - group, - admin.publicKey, - maxAccountNum + 1, - name, - ))!; - maxAccountNum = maxAccountNum + 1; + let mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); @@ -188,6 +196,108 @@ async function main() { } } + // Perp orders bring health <0, liquidator force closes + { + const name = 'LIQTEST, perp orders'; + + console.log(`Creating mangoaccount...`); + let mangoAccount = await createMangoAccount(name); + console.log( + `...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; + + await client.tokenDepositNative( + group, + mangoAccount, + collateralMint, + 100000, + ); // valued as $0.004 maint collateral + await mangoAccount.reload(client, group); + + await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 4); + + try { + await client.perpPlaceOrder( + group, + mangoAccount, + 'MNGO-PERP', + Side.bid, + 1, // ui price that won't get hit + 0.0011, // ui base quantity, 11 base lots, $0.044 + 0.044, // ui quote quantity + 4200, + PerpOrderType.limit, + 0, + 5, + ); + } finally { + await client.stubOracleSet(group, collateralOracle, PRICES['SOL']); + } + } + + // Perp base pos brings health<0, liquidator takes most of it + { + const name = 'LIQTEST, perp base pos'; + + console.log(`Creating mangoaccount...`); + let mangoAccount = await createMangoAccount(name); + console.log( + `...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; + + await client.tokenDepositNative( + group, + mangoAccount, + collateralMint, + 100000, + ); // valued as $0.004 maint collateral + await mangoAccount.reload(client, group); + + await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 5); + + try { + await client.perpPlaceOrder( + group, + fundingAccount, + 'MNGO-PERP', + Side.ask, + 40, + 0.0011, // ui base quantity, 11 base lots, $0.044 + 0.044, // ui quote quantity + 4200, + PerpOrderType.limit, + 0, + 5, + ); + + await client.perpPlaceOrder( + group, + mangoAccount, + 'MNGO-PERP', + Side.bid, + 40, + 0.0011, // ui base quantity, 11 base lots, $0.044 + 0.044, // ui quote quantity + 4200, + PerpOrderType.market, + 0, + 5, + ); + + await client.perpConsumeAllEvents(group, 'MNGO-PERP'); + } finally { + await client.stubOracleSet(group, collateralOracle, PRICES['SOL']); + } + } + process.exit(); } diff --git a/ts/client/src/scripts/mb-liqtest-settle-and-close-all.ts b/ts/client/src/scripts/mb-liqtest-settle-and-close-all.ts index d4907f1c1..5004bd356 100644 --- a/ts/client/src/scripts/mb-liqtest-settle-and-close-all.ts +++ b/ts/client/src/scripts/mb-liqtest-settle-and-close-all.ts @@ -1,7 +1,8 @@ -import { AnchorProvider, Wallet } from '@project-serum/anchor'; +import { BN, AnchorProvider, Wallet } from '@project-serum/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; import { MangoClient } from '../client'; +import { Side, PerpOrderType } from '../accounts/perp'; import { MANGO_V4_ID } from '../constants'; // @@ -57,63 +58,18 @@ async function main() { await client.serum3SettleFunds(group, account, serumExternal); await client.serum3CloseOpenOrders(group, account, serumExternal); } - } - accounts = await client.getMangoAccountsForOwner(group, admin.publicKey); - for (let account of accounts) { - console.log(`settling borrows on account: ${account}`); - - // first, settle all borrows - for (let token of account.tokensActive()) { - const bank = group.getFirstBankByTokenIndex(token.tokenIndex); - const amount = token.balance(bank).toNumber(); - if (amount < 0) { - try { - await client.tokenDepositNative( - group, - account, - bank.mint, - Math.ceil(-amount), - ); - await account.reload(client, group); - } catch (error) { - console.log( - `failed to deposit ${bank.name} into ${account.publicKey}: ${error}`, - ); - process.exit(); - } - } + for (let perpPosition of account.perpActive()) { + const perpMarket = group.findPerpMarket(perpPosition.marketIndex)!; + console.log( + `closing perp orders on: ${account} for market ${perpMarket.name}`, + ); + await client.perpCancelAllOrders(group, account, perpMarket.name, 10); } } accounts = await client.getMangoAccountsForOwner(group, admin.publicKey); for (let account of accounts) { - console.log(`withdrawing deposits of account: ${account}`); - - // withdraw all funds - for (let token of account.tokensActive()) { - const bank = group.getFirstBankByTokenIndex(token.tokenIndex); - const amount = token.balance(bank).toNumber(); - if (amount > 0) { - try { - const allowBorrow = false; - await client.tokenWithdrawNative( - group, - account, - bank.mint, - amount, - allowBorrow, - ); - await account.reload(client, group); - } catch (error) { - console.log( - `failed to withdraw ${bank.name} from ${account.publicKey}: ${error}`, - ); - process.exit(); - } - } - } - // close account try { console.log(`closing account: ${account}`); From 11daf4d0eb98a922fd9931265340b02e7bb482de Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Fri, 23 Sep 2022 11:59:18 +0200 Subject: [PATCH 18/39] client: fetch top settle pnl counterparties --- client/src/account_fetcher.rs | 107 ++++++++++++++++++--- client/src/chain_data.rs | 10 ++ client/src/chain_data_fetcher.rs | 28 +++++- client/src/client.rs | 63 +++++++++--- client/src/health_cache.rs | 33 +++++++ client/src/lib.rs | 2 + client/src/perp_pnl.rs | 103 ++++++++++++++++++++ liquidator/src/account_shared_data.rs | 30 ------ liquidator/src/liquidate.rs | 51 ++-------- liquidator/src/main.rs | 1 - liquidator/src/rebalance.rs | 5 +- programs/mango-v4/src/accounts_zerocopy.rs | 57 +++++++++++ 12 files changed, 385 insertions(+), 105 deletions(-) create mode 100644 client/src/health_cache.rs create mode 100644 client/src/perp_pnl.rs delete mode 100644 liquidator/src/account_shared_data.rs diff --git a/client/src/account_fetcher.rs b/client/src/account_fetcher.rs index 88bec46b6..7061cda79 100644 --- a/client/src/account_fetcher.rs +++ b/client/src/account_fetcher.rs @@ -7,22 +7,27 @@ use anchor_client::ClientError; use anchor_lang::AccountDeserialize; use solana_client::rpc_client::RpcClient; -use solana_sdk::account::Account; +use solana_sdk::account::{AccountSharedData, ReadableAccount}; use solana_sdk::pubkey::Pubkey; use mango_v4::state::MangoAccountValue; pub trait AccountFetcher: Sync + Send { - fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result; + fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result; + fn fetch_program_accounts( + &self, + program: &Pubkey, + discriminator: [u8; 8], + ) -> anyhow::Result>; } // Can't be in the trait, since then it would no longer be object-safe... pub fn account_fetcher_fetch_anchor_account( fetcher: &dyn AccountFetcher, - address: Pubkey, + address: &Pubkey, ) -> anyhow::Result { let account = fetcher.fetch_raw_account(address)?; - let mut data: &[u8] = &account.data; + let mut data: &[u8] = &account.data(); T::try_deserialize(&mut data) .with_context(|| format!("deserializing anchor account {}", address)) } @@ -30,10 +35,10 @@ pub fn account_fetcher_fetch_anchor_account( // Can't be in the trait, since then it would no longer be object-safe... pub fn account_fetcher_fetch_mango_account( fetcher: &dyn AccountFetcher, - address: Pubkey, + address: &Pubkey, ) -> anyhow::Result { let account = fetcher.fetch_raw_account(address)?; - let data: &[u8] = &account.data; + let data: &[u8] = &account.data(); MangoAccountValue::from_bytes(&data[8..]) .with_context(|| format!("deserializing mango account {}", address)) } @@ -43,26 +48,73 @@ pub struct RpcAccountFetcher { } impl AccountFetcher for RpcAccountFetcher { - fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result { + fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result { self.rpc - .get_account_with_commitment(&address, self.rpc.commitment()) - .with_context(|| format!("fetch account {}", address))? + .get_account_with_commitment(address, self.rpc.commitment()) + .with_context(|| format!("fetch account {}", *address))? .value .ok_or(ClientError::AccountNotFound) - .with_context(|| format!("fetch account {}", address)) + .with_context(|| format!("fetch account {}", *address)) + .map(Into::into) + } + + fn fetch_program_accounts( + &self, + program: &Pubkey, + discriminator: [u8; 8], + ) -> anyhow::Result> { + use solana_account_decoder::UiAccountEncoding; + use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}; + use solana_client::rpc_filter::{ + Memcmp, MemcmpEncodedBytes, MemcmpEncoding, RpcFilterType, + }; + let config = RpcProgramAccountsConfig { + filters: Some(vec![RpcFilterType::Memcmp(Memcmp { + offset: 0, + bytes: MemcmpEncodedBytes::Bytes(discriminator.to_vec()), + encoding: Some(MemcmpEncoding::Binary), + })]), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + commitment: Some(self.rpc.commitment()), + ..RpcAccountInfoConfig::default() + }, + with_context: Some(true), + }; + let accs = self.rpc.get_program_accounts_with_config(program, config)?; + // convert Account -> AccountSharedData + Ok(accs + .into_iter() + .map(|(pk, acc)| (pk, acc.into())) + .collect::>()) + } +} + +struct AccountCache { + accounts: HashMap, + keys_for_program_and_discriminator: HashMap<(Pubkey, [u8; 8]), Vec>, +} + +impl AccountCache { + fn clear(&mut self) { + self.accounts.clear(); + self.keys_for_program_and_discriminator.clear(); } } pub struct CachedAccountFetcher { fetcher: T, - cache: Mutex>, + cache: Mutex, } impl CachedAccountFetcher { pub fn new(fetcher: T) -> Self { Self { fetcher, - cache: Mutex::new(HashMap::new()), + cache: Mutex::new(AccountCache { + accounts: HashMap::new(), + keys_for_program_and_discriminator: HashMap::new(), + }), } } @@ -73,13 +125,38 @@ impl CachedAccountFetcher { } impl AccountFetcher for CachedAccountFetcher { - fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result { + fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result { let mut cache = self.cache.lock().unwrap(); - if let Some(account) = cache.get(&address) { + if let Some(account) = cache.accounts.get(address) { return Ok(account.clone()); } let account = self.fetcher.fetch_raw_account(address)?; - cache.insert(address, account.clone()); + cache.accounts.insert(*address, account.clone()); Ok(account) } + + fn fetch_program_accounts( + &self, + program: &Pubkey, + discriminator: [u8; 8], + ) -> anyhow::Result> { + let cache_key = (*program, discriminator); + let mut cache = self.cache.lock().unwrap(); + if let Some(accounts) = cache.keys_for_program_and_discriminator.get(&cache_key) { + return Ok(accounts + .iter() + .map(|pk| (*pk, cache.accounts.get(&pk).unwrap().clone())) + .collect::>()); + } + let accounts = self + .fetcher + .fetch_program_accounts(program, discriminator)?; + cache + .keys_for_program_and_discriminator + .insert(cache_key, accounts.iter().map(|(pk, _)| *pk).collect()); + for (pk, acc) in accounts.iter() { + cache.accounts.insert(*pk, acc.clone()); + } + Ok(accounts) + } } diff --git a/client/src/chain_data.rs b/client/src/chain_data.rs index 1241bd274..e875ca659 100644 --- a/client/src/chain_data.rs +++ b/client/src/chain_data.rs @@ -224,6 +224,16 @@ impl ChainData { .ok_or_else(|| anyhow::anyhow!("account {} has no live data", pubkey)) } + pub fn iter_accounts<'a>(&'a self) -> impl Iterator { + self.accounts.iter().filter_map(|(pk, writes)| { + writes + .iter() + .rev() + .find(|w| self.is_account_write_live(w)) + .map(|latest_write| (pk, latest_write)) + }) + } + pub fn slots_count(&self) -> usize { self.slots.len() } diff --git a/client/src/chain_data_fetcher.rs b/client/src/chain_data_fetcher.rs index 26f7f8221..caabb322e 100644 --- a/client/src/chain_data_fetcher.rs +++ b/client/src/chain_data_fetcher.rs @@ -138,7 +138,31 @@ impl AccountFetcher { } impl crate::AccountFetcher for AccountFetcher { - fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result { - self.fetch_raw(&address).map(|a| a.into()) + fn fetch_raw_account( + &self, + address: &Pubkey, + ) -> anyhow::Result { + self.fetch_raw(&address) + } + + fn fetch_program_accounts( + &self, + program: &Pubkey, + discriminator: [u8; 8], + ) -> anyhow::Result> { + let chain_data = self.chain_data.read().unwrap(); + Ok(chain_data + .iter_accounts() + .filter_map(|(pk, data)| { + if data.account.owner() != program { + return None; + } + let acc_data = data.account.data(); + if acc_data.len() < 8 || acc_data[..8] != discriminator { + return None; + } + Some((*pk, data.account.clone())) + }) + .collect::>()) } } diff --git a/client/src/client.rs b/client/src/client.rs index 0170f7bed..910c7df44 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -24,12 +24,13 @@ use solana_client::rpc_client::RpcClient; use solana_sdk::signer::keypair; use crate::account_fetcher::*; -use crate::context::{MangoGroupContext, PerpMarketContext, Serum3MarketContext, TokenContext}; +use crate::context::{MangoGroupContext, Serum3MarketContext, TokenContext}; use crate::gpa::fetch_mango_accounts; use crate::jupiter; use crate::util::MyClone; use anyhow::Context; +use solana_sdk::account::ReadableAccount; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::signature::{Keypair, Signature}; use solana_sdk::sysvar; @@ -230,7 +231,7 @@ impl MangoClient { ) -> anyhow::Result { let rpc = client.rpc(); 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)?; let group = mango_account.fixed.group; if mango_account.fixed.owner != owner.pubkey() { anyhow::bail!( @@ -290,12 +291,12 @@ impl MangoClient { } pub fn mango_account(&self) -> anyhow::Result { - account_fetcher_fetch_mango_account(&*self.account_fetcher, self.mango_account_address) + account_fetcher_fetch_mango_account(&*self.account_fetcher, &self.mango_account_address) } pub 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) } pub fn derive_health_check_remaining_account_metas( @@ -461,8 +462,8 @@ impl MangoClient { ) -> 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)?; - Ok(pyth_sdk_solana::load_price(&oracle_account.data).unwrap()) + let oracle_account = self.account_fetcher.fetch_raw_account(&mint_info.oracle)?; + Ok(pyth_sdk_solana::load_price(&oracle_account.data()).unwrap()) } // @@ -719,8 +720,8 @@ impl MangoClient { .unwrap(); let account = self.mango_account()?; let open_orders = account.serum3_orders(market_index).unwrap().open_orders; - - let open_orders_bytes = self.account_fetcher.fetch_raw_account(open_orders)?.data; + let open_orders_acc = self.account_fetcher.fetch_raw_account(&open_orders)?; + 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::()], ); @@ -838,11 +839,45 @@ impl MangoClient { // // Perps // - fn perp_data_by_market_index( + pub fn perp_settle_pnl( &self, market_index: PerpMarketIndex, - ) -> Result<&PerpMarketContext, ClientError> { - Ok(self.context.perp_markets.get(&market_index).unwrap()) + account_a: &Pubkey, + account_b: (&Pubkey, &MangoAccountValue), + ) -> anyhow::Result { + let perp = self.context.perp(market_index); + let settlement_token = self.context.token(0); + + let health_remaining_ams = self + .context + .derive_health_check_remaining_account_metas(account_b.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::PerpSettlePnl { + group: self.group(), + perp_market: perp.address, + account_a: *account_a, + account_b: *account_b.0, + oracle: perp.market.oracle, + quote_bank: settlement_token.mint_info.first_bank(), + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpSettlePnl { + max_settle_amount: u64::MAX, + }), + }) + .send() + .map_err(prettify_client_error) } pub fn perp_liq_force_cancel_orders( @@ -850,7 +885,7 @@ impl MangoClient { liqee: (&Pubkey, &MangoAccountValue), market_index: PerpMarketIndex, ) -> anyhow::Result { - let perp = self.perp_data_by_market_index(market_index)?; + let perp = self.context.perp(market_index); let health_remaining_ams = self .context @@ -890,7 +925,7 @@ impl MangoClient { market_index: PerpMarketIndex, max_base_transfer: i64, ) -> anyhow::Result { - let perp = self.perp_data_by_market_index(market_index)?; + let perp = self.context.perp(market_index); let health_remaining_ams = self .context @@ -1002,7 +1037,7 @@ impl MangoClient { let group = account_fetcher_fetch_anchor_account::( &*self.account_fetcher, - self.context.group, + &self.context.group, )?; self.program() diff --git a/client/src/health_cache.rs b/client/src/health_cache.rs new file mode 100644 index 000000000..03b24e58b --- /dev/null +++ b/client/src/health_cache.rs @@ -0,0 +1,33 @@ +use crate::{AccountFetcher, MangoGroupContext}; +use anyhow::Context; +use mango_v4::accounts_zerocopy::KeyedAccountSharedData; +use mango_v4::state::{FixedOrderAccountRetriever, HealthCache, MangoAccountValue}; + +pub fn new( + context: &MangoGroupContext, + account_fetcher: &impl AccountFetcher, + account: &MangoAccountValue, +) -> anyhow::Result { + let active_token_len = account.active_token_positions().count(); + 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| { + Ok(KeyedAccountSharedData::new( + meta.pubkey, + account_fetcher.fetch_raw_account(&meta.pubkey)?, + )) + }) + .collect::>>()?; + + let retriever = FixedOrderAccountRetriever { + ais: accounts, + n_banks: active_token_len, + n_perps: active_perp_len, + begin_perp: active_token_len * 2, + begin_serum3: active_token_len * 2 + active_perp_len, + }; + mango_v4::state::new_health_cache(&account.borrow(), &retriever).context("make health cache") +} diff --git a/client/src/lib.rs b/client/src/lib.rs index 5dcedaef8..eef1b25aa 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -9,5 +9,7 @@ mod chain_data_fetcher; mod client; mod context; mod gpa; +pub mod health_cache; mod jupiter; +pub mod perp_pnl; mod util; diff --git a/client/src/perp_pnl.rs b/client/src/perp_pnl.rs new file mode 100644 index 000000000..d69063ecc --- /dev/null +++ b/client/src/perp_pnl.rs @@ -0,0 +1,103 @@ +use anchor_lang::prelude::*; +use anchor_lang::Discriminator; +use fixed::types::I80F48; +use solana_sdk::account::ReadableAccount; + +use crate::*; +use mango_v4::accounts_zerocopy::KeyedAccountSharedData; +use mango_v4::state::*; + +#[derive(Debug, PartialEq)] +pub enum Direction { + MaxPositive, + MaxNegative, +} + +/// Returns up to `count` accounts with highest abs pnl (by `direction`) in descending order. +pub fn fetch_top( + context: &crate::context::MangoGroupContext, + account_fetcher: &impl AccountFetcher, + perp_market_index: PerpMarketIndex, + perp_market_address: &Pubkey, + direction: Direction, + count: usize, +) -> anyhow::Result> { + let perp_market = + account_fetcher_fetch_anchor_account::(account_fetcher, perp_market_address)?; + let oracle_acc = account_fetcher.fetch_raw_account(&perp_market.oracle)?; + let oracle_price = + perp_market.oracle_price(&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc))?; + + let accounts = + account_fetcher.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())?; + + let mut accounts_pnl = accounts + .iter() + .filter_map(|(pk, acc)| { + let data = acc.data(); + let mango_acc = MangoAccountValue::from_bytes(&data[8..]); + if mango_acc.is_err() { + return None; + } + let mango_acc = mango_acc.unwrap(); + let perp_pos = mango_acc.perp_position(perp_market_index); + if perp_pos.is_err() { + return None; + } + let perp_pos = perp_pos.unwrap(); + let pnl = perp_pos.base_position_native(&perp_market) * oracle_price + + perp_pos.quote_position_native(); + if pnl >= 0 && direction == Direction::MaxNegative + || pnl <= 0 && direction == Direction::MaxPositive + { + return None; + } + Some((*pk, mango_acc, pnl)) + }) + .collect::>(); + + // Sort the top accounts to the front + match direction { + Direction::MaxPositive => { + accounts_pnl.sort_by(|a, b| b.2.cmp(&a.2)); + } + Direction::MaxNegative => { + accounts_pnl.sort_by(|a, b| a.2.cmp(&b.2)); + } + } + + // Negative pnl needs to be limited by spot_health. + // We're doing it in a second step, because it's pretty expensive and we don't + // want to run this for all accounts. + if direction == Direction::MaxNegative { + let mut stable = 0; + for i in 0..accounts_pnl.len() { + let (_, acc, pnl) = &accounts_pnl[i]; + let next_pnl = if i + 1 < accounts_pnl.len() { + accounts_pnl[i + 1].2 + } else { + I80F48::ZERO + }; + let spot_health = crate::health_cache::new(context, account_fetcher, &acc)? + .spot_health(HealthType::Maint); + let settleable_pnl = if spot_health > 0 && !acc.being_liquidated() { + (*pnl).max(-spot_health) + } else { + I80F48::ZERO + }; + accounts_pnl[i].2 = settleable_pnl; + + // if the ordering was unchanged `count` times we know we have the top `count` accounts + if settleable_pnl <= next_pnl { + stable += 1; + if stable >= count { + break; + } + } + } + accounts_pnl.sort_by(|a, b| a.2.cmp(&b.2)); + } + + // return highest abs pnl accounts + Ok(accounts_pnl[0..count].to_vec()) +} diff --git a/liquidator/src/account_shared_data.rs b/liquidator/src/account_shared_data.rs deleted file mode 100644 index c6e7f534a..000000000 --- a/liquidator/src/account_shared_data.rs +++ /dev/null @@ -1,30 +0,0 @@ -use mango_v4::accounts_zerocopy::{AccountReader, KeyedAccountReader}; -use solana_sdk::{account::AccountSharedData, pubkey::Pubkey}; - -#[derive(Clone)] -pub struct KeyedAccountSharedData { - pub key: Pubkey, - pub data: AccountSharedData, -} - -impl KeyedAccountSharedData { - pub fn new(key: Pubkey, data: AccountSharedData) -> Self { - Self { key, data } - } -} - -impl AccountReader for KeyedAccountSharedData { - fn owner(&self) -> &Pubkey { - AccountReader::owner(&self.data) - } - - fn data(&self) -> &[u8] { - AccountReader::data(&self.data) - } -} - -impl KeyedAccountReader for KeyedAccountSharedData { - fn key(&self) -> &Pubkey { - &self.key - } -} diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 1572d2614..02a9f01d5 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -1,11 +1,9 @@ use std::time::Duration; -use crate::account_shared_data::KeyedAccountSharedData; - -use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext}; +use client::{chain_data, health_cache, AccountFetcher, MangoClient, MangoClientError}; +use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::state::{ - new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue, - PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX, + Bank, HealthType, PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX, }; use itertools::Itertools; @@ -17,35 +15,6 @@ pub struct Config { pub refresh_timeout: Duration, } -pub fn new_health_cache_( - context: &MangoGroupContext, - account_fetcher: &chain_data::AccountFetcher, - account: &MangoAccountValue, -) -> anyhow::Result { - let active_token_len = account.active_token_positions().count(); - 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| { - Ok(KeyedAccountSharedData::new( - meta.pubkey, - account_fetcher.fetch_raw(&meta.pubkey)?, - )) - }) - .collect::>>()?; - - let retriever = FixedOrderAccountRetriever { - ais: accounts, - n_banks: active_token_len, - n_perps: active_perp_len, - begin_perp: active_token_len * 2, - begin_serum3: active_token_len * 2 + active_perp_len, - }; - new_health_cache(&account.borrow(), &retriever).context("make health cache") -} - pub fn jupiter_market_can_buy( mango_client: &MangoClient, token: TokenIndex, @@ -112,7 +81,7 @@ pub fn maybe_liquidate_account( let account = account_fetcher.fetch_mango_account(pubkey)?; let health_cache = - new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok"); + health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok"); let maint_health = health_cache.health(HealthType::Maint); if !health_cache.is_liquidatable() { return Ok(false); @@ -130,7 +99,7 @@ pub fn maybe_liquidate_account( // be great at providing timely updates to the account data. let account = account_fetcher.fetch_fresh_mango_account(pubkey)?; let health_cache = - new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok"); + health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok"); if !health_cache.is_liquidatable() { return Ok(false); } @@ -145,7 +114,7 @@ pub fn maybe_liquidate_account( .map(|token_position| { let token = mango_client.context.token(token_position.token_index); let bank = account_fetcher.fetch::(&token.mint_info.first_bank())?; - let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?; + let oracle = account_fetcher.fetch_raw_account(&token.mint_info.oracle)?; let price = bank.oracle_price(&KeyedAccountSharedData::new( token.mint_info.oracle, oracle.into(), @@ -163,7 +132,7 @@ pub fn maybe_liquidate_account( let serum_force_cancels = account .active_serum3_orders() .map(|orders| { - let open_orders_account = account_fetcher.fetch_raw_account(orders.open_orders)?; + let open_orders_account = account_fetcher.fetch_raw_account(&orders.open_orders)?; let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?; let can_force_cancel = open_orders.native_coin_total > 0 || open_orders.native_pc_total > 0 @@ -190,7 +159,7 @@ pub fn maybe_liquidate_account( return Ok(None); } let perp = mango_client.context.perp(pp.market_index); - let oracle = account_fetcher.fetch_raw_account(perp.market.oracle)?; + let oracle = account_fetcher.fetch_raw_account(&perp.market.oracle)?; let price = perp.market.oracle_price(&KeyedAccountSharedData::new( perp.market.oracle, oracle.into(), @@ -218,7 +187,7 @@ pub fn maybe_liquidate_account( liqor.ensure_token_position(target)?; let health_cache = - new_health_cache_(&mango_client.context, account_fetcher, &liqor).expect("always ok"); + health_cache::new(&mango_client.context, account_fetcher, &liqor).expect("always ok"); let source_price = health_cache.token_info(source).unwrap().oracle_price; let target_price = health_cache.token_info(target).unwrap().oracle_price; @@ -278,7 +247,7 @@ pub fn maybe_liquidate_account( .fetch_fresh_mango_account(&mango_client.mango_account_address) .context("getting liquidator account")?; liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?; - let health_cache = new_health_cache_(&mango_client.context, account_fetcher, &liqor) + let health_cache = health_cache::new(&mango_client.context, account_fetcher, &liqor) .expect("always ok"); health_cache.max_perp_for_health_ratio( *perp_market_index, diff --git a/liquidator/src/main.rs b/liquidator/src/main.rs index faf84ce1c..77120fd90 100644 --- a/liquidator/src/main.rs +++ b/liquidator/src/main.rs @@ -13,7 +13,6 @@ use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::pubkey::Pubkey; use std::collections::HashSet; -pub mod account_shared_data; pub mod liquidate; pub mod metrics; pub mod rebalance; diff --git a/liquidator/src/rebalance.rs b/liquidator/src/rebalance.rs index 9cdcbb76d..3a969139e 100644 --- a/liquidator/src/rebalance.rs +++ b/liquidator/src/rebalance.rs @@ -1,6 +1,7 @@ -use crate::{account_shared_data::KeyedAccountSharedData, AnyhowWrap}; +use crate::AnyhowWrap; use client::{chain_data, AccountFetcher, MangoClient, TokenContext}; +use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::state::{Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX}; use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey}; @@ -43,7 +44,7 @@ impl TokenState { 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)?; bank.oracle_price(&KeyedAccountSharedData::new( token.mint_info.oracle, oracle.into(), diff --git a/programs/mango-v4/src/accounts_zerocopy.rs b/programs/mango-v4/src/accounts_zerocopy.rs index e1fb765f4..6ec30ae2d 100644 --- a/programs/mango-v4/src/accounts_zerocopy.rs +++ b/programs/mango-v4/src/accounts_zerocopy.rs @@ -110,6 +110,63 @@ impl AccountReader for T { } } +#[cfg(feature = "solana-sdk")] +#[derive(Clone)] +pub struct KeyedAccount { + pub key: Pubkey, + pub account: solana_sdk::account::Account, +} + +#[cfg(feature = "solana-sdk")] +impl AccountReader for KeyedAccount { + fn owner(&self) -> &Pubkey { + self.account.owner() + } + + fn data(&self) -> &[u8] { + self.account.data() + } +} + +#[cfg(feature = "solana-sdk")] +impl KeyedAccountReader for KeyedAccount { + fn key(&self) -> &Pubkey { + &self.key + } +} + +#[cfg(feature = "solana-sdk")] +#[derive(Clone)] +pub struct KeyedAccountSharedData { + pub key: Pubkey, + pub data: solana_sdk::account::AccountSharedData, +} + +#[cfg(feature = "solana-sdk")] +impl KeyedAccountSharedData { + pub fn new(key: Pubkey, data: solana_sdk::account::AccountSharedData) -> Self { + Self { key, data } + } +} + +#[cfg(feature = "solana-sdk")] +impl AccountReader for KeyedAccountSharedData { + fn owner(&self) -> &Pubkey { + AccountReader::owner(&self.data) + } + + fn data(&self) -> &[u8] { + AccountReader::data(&self.data) + } +} + +#[cfg(feature = "solana-sdk")] +impl KeyedAccountReader for KeyedAccountSharedData { + fn key(&self) -> &Pubkey { + &self.key + } +} + // // Common traits for loading from account data. // From 15d0a98c945d4957a299a8c94aed348f3561d605 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 26 Sep 2022 13:27:31 +0200 Subject: [PATCH 19/39] liquidator: settle pnl on liquidatable accounts --- client/src/perp_pnl.rs | 6 +- liquidator/src/liquidate.rs | 718 +++++++++++------- .../src/scripts/mb-liqtest-make-candidates.ts | 104 +++ 3 files changed, 566 insertions(+), 262 deletions(-) diff --git a/client/src/perp_pnl.rs b/client/src/perp_pnl.rs index d69063ecc..22b1b3feb 100644 --- a/client/src/perp_pnl.rs +++ b/client/src/perp_pnl.rs @@ -18,12 +18,12 @@ pub fn fetch_top( context: &crate::context::MangoGroupContext, account_fetcher: &impl AccountFetcher, perp_market_index: PerpMarketIndex, - perp_market_address: &Pubkey, direction: Direction, count: usize, ) -> anyhow::Result> { + let perp = context.perp(perp_market_index); let perp_market = - account_fetcher_fetch_anchor_account::(account_fetcher, perp_market_address)?; + account_fetcher_fetch_anchor_account::(account_fetcher, &perp.address)?; let oracle_acc = account_fetcher.fetch_raw_account(&perp_market.oracle)?; let oracle_price = perp_market.oracle_price(&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc))?; @@ -99,5 +99,5 @@ pub fn fetch_top( } // return highest abs pnl accounts - Ok(accounts_pnl[0..count].to_vec()) + Ok(accounts_pnl.into_iter().take(count).collect::>()) } diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 02a9f01d5..7b09eddcb 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -3,8 +3,10 @@ use std::time::Duration; use client::{chain_data, health_cache, AccountFetcher, MangoClient, MangoClientError}; use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::state::{ - Bank, HealthType, PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX, + Bank, HealthCache, HealthType, MangoAccountValue, PerpMarketIndex, Serum3Orders, Side, + TokenIndex, QUOTE_TOKEN_INDEX, }; +use solana_sdk::signature::Signature; use itertools::Itertools; use rand::seq::SliceRandom; @@ -69,6 +71,451 @@ pub fn jupiter_market_can_sell( .is_ok() } +struct LiquidateHelper<'a> { + client: &'a MangoClient, + account_fetcher: &'a chain_data::AccountFetcher, + pubkey: &'a Pubkey, + liqee: &'a MangoAccountValue, + health_cache: &'a HealthCache, + maint_health: I80F48, + liqor_min_health_ratio: I80F48, +} + +impl<'a> LiquidateHelper<'a> { + 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 open_orders_account = self + .account_fetcher + .fetch_raw_account(&orders.open_orders)?; + let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?; + 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)) + } else { + Ok(None) + } + }) + .filter_map_ok(|v| v) + .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, + )?; + log::info!( + "Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}", + self.pubkey, + serum_orders.market_index, + self.maint_health, + sig + ); + Ok(Some(sig)) + } + + fn perp_close_orders(&self) -> anyhow::Result> { + let perp_force_cancels = self + .liqee + .active_perp_positions() + .filter_map(|pp| pp.has_open_orders().then(|| pp.market_index)) + .collect::>(); + if perp_force_cancels.is_empty() { + return Ok(None); + } + + // Cancel all orders on a random perp market + 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)?; + log::info!( + "Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}", + self.pubkey, + perp_market_index, + self.maint_health, + sig + ); + Ok(Some(sig)) + } + + fn liq_perp_base_position(&self) -> anyhow::Result> { + let mut perp_base_positions = self + .liqee + .active_perp_positions() + .map(|pp| { + let base_lots = pp.base_position_lots(); + if base_lots == 0 { + return Ok(None); + } + let perp = self.client.context.perp(pp.market_index); + let oracle = self + .account_fetcher + .fetch_raw_account(&perp.market.oracle)?; + let price = perp.market.oracle_price(&KeyedAccountSharedData::new( + perp.market.oracle, + oracle.into(), + ))?; + Ok(Some(( + pp.market_index, + base_lots, + price, + I80F48::from(base_lots.abs()) * price, + ))) + }) + .filter_map_ok(|v| v) + .collect::>>()?; + perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3)); + + if perp_base_positions.is_empty() { + return Ok(None); + } + + // Liquidate the highest-value perp base position + let (perp_market_index, base_lots, price, _) = perp_base_positions.last().unwrap(); + let perp = self.client.context.perp(*perp_market_index); + + let (side, side_signum) = if *base_lots > 0 { + (Side::Bid, 1) + } else { + (Side::Ask, -1) + }; + + // Compute the max number of base_lots the liqor is willing to take + let max_base_transfer_abs = { + let mut liqor = self + .account_fetcher + .fetch_fresh_mango_account(&self.client.mango_account_address) + .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) + .expect("always ok"); + health_cache.max_perp_for_health_ratio( + *perp_market_index, + *price, + perp.market.base_lot_size, + side, + self.liqor_min_health_ratio, + )? + }; + 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, + )?; + log::info!( + "Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}", + self.pubkey, + perp_market_index, + self.maint_health, + sig + ); + Ok(Some(sig)) + } + + fn settle_perp_pnl(&self) -> anyhow::Result> { + let spot_health = self.health_cache.spot_health(HealthType::Maint); + let mut perp_settleable_pnl = self + .liqee + .active_perp_positions() + .map(|pp| { + if pp.base_position_lots() != 0 { + return Ok(None); + } + let pnl = pp.quote_position_native(); + let settleable_pnl = if pnl > 0 { + pnl + } else if pnl < 0 && spot_health > 0 { + pnl.max(-spot_health) + } else { + return Ok(None); + }; + Ok(Some((pp.market_index, settleable_pnl))) + }) + .filter_map_ok(|v| v) + .collect::>>()?; + // sort by pnl, descending + perp_settleable_pnl.sort_by(|a, b| b.1.cmp(&a.1)); + + if perp_settleable_pnl.is_empty() { + return Ok(None); + } + + for (perp_index, pnl) in perp_settleable_pnl { + let direction = if pnl > 0 { + client::perp_pnl::Direction::MaxNegative + } else { + client::perp_pnl::Direction::MaxPositive + }; + let counters = client::perp_pnl::fetch_top( + &self.client.context, + self.account_fetcher, + perp_index, + direction, + 2, + )?; + 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 + log::info!("Could not settle perp pnl {pnl} for account {}, perp market {perp_index}: no counterparty", + self.pubkey); + continue; + } + let (counter_key, counter_acc, _) = counters.first().unwrap(); + + let (account_a, account_b) = if pnl > 0 { + (self.pubkey, (counter_key, counter_acc)) + } else { + (counter_key, (self.pubkey, self.liqee)) + }; + let sig = self + .client + .perp_settle_pnl(perp_index, account_a, account_b)?; + log::info!( + "Settled perp pnl for perp market on account {}, market index {perp_index}, maint_health was {}, tx sig {sig:?}", + self.pubkey, + self.maint_health, + ); + return Ok(Some(sig)); + } + return Ok(None); + } + + 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(), + ))?; + Ok(( + token_position.token_index, + price, + token_position.native(&bank) * price, + )) + }) + .collect::>>()?; + tokens.sort_by(|a, b| a.2.cmp(&b.2)); + Ok(tokens) + } + + fn max_token_liab_transfer( + &self, + source: TokenIndex, + target: TokenIndex, + ) -> anyhow::Result { + let mut liqor = self + .account_fetcher + .fetch_fresh_mango_account(&self.client.mango_account_address) + .context("getting liquidator account")?; + + // Ensure the tokens are activated, so they appear in the health cache and + // max_swap_source() will work. + liqor.ensure_token_position(source)?; + liqor.ensure_token_position(target)?; + + let health_cache = health_cache::new(&self.client.context, self.account_fetcher, &liqor) + .expect("always ok"); + + let source_price = health_cache.token_info(source).unwrap().oracle_price; + let target_price = health_cache.token_info(target).unwrap().oracle_price; + // TODO: This is where we could multiply in the liquidation fee factors + let oracle_swap_price = source_price / target_price; + + let amount = health_cache + .max_swap_source_for_health_ratio( + source, + target, + oracle_swap_price, + self.liqor_min_health_ratio, + ) + .context("getting max_swap_source")?; + Ok(amount) + } + + fn liq_spot(&self) -> anyhow::Result> { + if !self.health_cache.has_borrows() || self.health_cache.can_call_spot_bankruptcy() { + return Ok(None); + } + + let tokens = self.tokens()?; + + 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) + }) + .ok_or_else(|| { + anyhow::anyhow!( + "mango account {}, has no asset tokens that are sellable for USDC: {:?}", + self.pubkey, + tokens + ) + })? + .0; + let liab_token_index = tokens + .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) + }) + .ok_or_else(|| { + anyhow::anyhow!( + "mango account {}, has no liab tokens that are purchasable for USDC: {:?}", + self.pubkey, + tokens + ) + })? + .0; + + let max_liab_transfer = self + .max_token_liab_transfer(liab_token_index, asset_token_index) + .context("getting max_liab_transfer")?; + + // + // TODO: log liqor's assets in UI form + // TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side + // + let sig = self + .client + .token_liq_with_token( + (self.pubkey, &self.liqee), + asset_token_index, + liab_token_index, + max_liab_transfer, + ) + .context("sending liq_token_with_token")?; + log::info!( + "Liquidated token with token for {}, maint_health was {}, tx sig {:?}", + self.pubkey, + self.maint_health, + sig + ); + Ok(Some(sig)) + } + + fn bankrupt_spot(&self) -> anyhow::Result> { + if !self.health_cache.can_call_spot_bankruptcy() { + return Ok(None); + } + + let tokens = self.tokens()?; + + if tokens.is_empty() { + anyhow::bail!( + "mango account {}, is bankrupt has no active tokens", + self.pubkey + ); + } + let liab_token_index = tokens + .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) + }) + .ok_or_else(|| { + anyhow::anyhow!( + "mango account {}, has no liab tokens that are purchasable for USDC: {:?}", + self.pubkey, + tokens + ) + })? + .0; + + let quote_token_index = 0; + let max_liab_transfer = + self.max_token_liab_transfer(liab_token_index, quote_token_index)?; + + let sig = self + .client + .token_liq_bankruptcy( + (self.pubkey, &self.liqee), + liab_token_index, + max_liab_transfer, + ) + .context("sending liq_token_bankruptcy")?; + log::info!( + "Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}", + self.pubkey, + self.maint_health, + sig + ); + Ok(Some(sig)) + } + + 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. + // We shouldn't get stuck on this step, particularly if it's of limited value + // to the liquidators. + // if let Some(txsig) = self.perp_settle_positive_pnl()? { + // return Ok(txsig); + // } + + // Try to close orders before touching the user's positions + if let Some(txsig) = self.perp_close_orders()? { + return Ok(txsig); + } + if let Some(txsig) = self.serum3_close_orders()? { + return Ok(txsig); + } + + if let Some(txsig) = self.liq_perp_base_position()? { + return Ok(txsig); + } + + // Now that the perp base positions are zeroed the perp pnl won't + // fluctuate with the oracle price anymore. + // 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.settle_perp_pnl()? { + return Ok(txsig); + } + + if let Some(txsig) = self.liq_spot()? { + return Ok(txsig); + } + + // TODO: socialize unsettleable negative pnl + // if let Some(txsig) = self.bankrupt_perp()? { + // return Ok(txsig); + // } + if let Some(txsig) = self.bankrupt_spot()? { + return Ok(txsig); + } + anyhow::bail!( + "Don't know what to do with liquidatable account {}, maint_health was {}", + self.pubkey, + self.maint_health + ); + } +} + #[allow(clippy::too_many_arguments)] pub fn maybe_liquidate_account( mango_client: &MangoClient, @@ -76,8 +523,7 @@ pub fn maybe_liquidate_account( pubkey: &Pubkey, config: &Config, ) -> anyhow::Result { - let min_health_ratio = I80F48::from_num(config.min_health_ratio); - let quote_token_index = 0; + let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio); let account = account_fetcher.fetch_mango_account(pubkey)?; let health_cache = @@ -105,264 +551,18 @@ pub fn maybe_liquidate_account( } let maint_health = health_cache.health(HealthType::Maint); - let is_spot_bankrupt = health_cache.can_call_spot_bankruptcy(); - let is_spot_liquidatable = health_cache.has_borrows() && !is_spot_bankrupt; - - // find asset and liab tokens - let mut tokens = account - .active_token_positions() - .map(|token_position| { - let token = mango_client.context.token(token_position.token_index); - let bank = account_fetcher.fetch::(&token.mint_info.first_bank())?; - let oracle = account_fetcher.fetch_raw_account(&token.mint_info.oracle)?; - let price = bank.oracle_price(&KeyedAccountSharedData::new( - token.mint_info.oracle, - oracle.into(), - ))?; - Ok(( - token_position.token_index, - price, - token_position.native(&bank) * price, - )) - }) - .collect::>>()?; - tokens.sort_by(|a, b| a.2.cmp(&b.2)); - - // look for any open serum orders or settleable balances - let serum_force_cancels = account - .active_serum3_orders() - .map(|orders| { - let open_orders_account = account_fetcher.fetch_raw_account(&orders.open_orders)?; - let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?; - 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)) - } else { - Ok(None) - } - }) - .filter_map_ok(|v| v) - .collect::>>()?; - - // look for any perp open orders and base positions - let perp_force_cancels = account - .active_perp_positions() - .filter_map(|pp| pp.has_open_orders().then(|| pp.market_index)) - .collect::>(); - let mut perp_base_positions = account - .active_perp_positions() - .map(|pp| { - let base_lots = pp.base_position_lots(); - if base_lots == 0 { - return Ok(None); - } - let perp = mango_client.context.perp(pp.market_index); - let oracle = account_fetcher.fetch_raw_account(&perp.market.oracle)?; - let price = perp.market.oracle_price(&KeyedAccountSharedData::new( - perp.market.oracle, - oracle.into(), - ))?; - Ok(Some(( - pp.market_index, - base_lots, - price, - I80F48::from(base_lots.abs()) * price, - ))) - }) - .filter_map_ok(|v| v) - .collect::>>()?; - // sort by base_position_value, ascending - perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3)); - - let get_max_liab_transfer = |source, target| -> anyhow::Result { - let mut liqor = account_fetcher - .fetch_fresh_mango_account(&mango_client.mango_account_address) - .context("getting liquidator account")?; - - // Ensure the tokens are activated, so they appear in the health cache and - // max_swap_source() will work. - liqor.ensure_token_position(source)?; - liqor.ensure_token_position(target)?; - - let health_cache = - health_cache::new(&mango_client.context, account_fetcher, &liqor).expect("always ok"); - - let source_price = health_cache.token_info(source).unwrap().oracle_price; - let target_price = health_cache.token_info(target).unwrap().oracle_price; - // TODO: This is where we could multiply in the liquidation fee factors - let oracle_swap_price = source_price / target_price; - - let amount = health_cache - .max_swap_source_for_health_ratio(source, target, oracle_swap_price, min_health_ratio) - .context("getting max_swap_source")?; - Ok(amount) - }; // try liquidating - let txsig = if !serum_force_cancels.is_empty() { - // Cancel all orders on a random serum market - let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap(); - let sig = mango_client.serum3_liq_force_cancel_orders( - (pubkey, &account), - serum_orders.market_index, - &serum_orders.open_orders, - )?; - log::info!( - "Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}", - pubkey, - serum_orders.market_index, - maint_health, - sig - ); - sig - } else if !perp_force_cancels.is_empty() { - // Cancel all orders on a random perp market - let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap(); - let sig = - mango_client.perp_liq_force_cancel_orders((pubkey, &account), perp_market_index)?; - log::info!( - "Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}", - pubkey, - perp_market_index, - maint_health, - sig - ); - sig - } else if !perp_base_positions.is_empty() { - // Liquidate the highest-value perp base position - let (perp_market_index, base_lots, price, _) = perp_base_positions.last().unwrap(); - let perp = mango_client.context.perp(*perp_market_index); - - let (side, side_signum) = if *base_lots > 0 { - (Side::Bid, 1) - } else { - (Side::Ask, -1) - }; - - // Compute the max number of base_lots the liqor is willing to take - let max_base_transfer_abs = { - let mut liqor = account_fetcher - .fetch_fresh_mango_account(&mango_client.mango_account_address) - .context("getting liquidator account")?; - liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?; - let health_cache = health_cache::new(&mango_client.context, account_fetcher, &liqor) - .expect("always ok"); - health_cache.max_perp_for_health_ratio( - *perp_market_index, - *price, - perp.market.base_lot_size, - side, - min_health_ratio, - )? - }; - log::info!("computed max_base_transfer to be {max_base_transfer_abs}"); - - let sig = mango_client.perp_liq_base_position( - (pubkey, &account), - *perp_market_index, - side_signum * max_base_transfer_abs, - )?; - log::info!( - "Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}", - pubkey, - perp_market_index, - maint_health, - sig - ); - sig - } else if is_spot_bankrupt { - if tokens.is_empty() { - anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey); - } - let liab_token_index = tokens - .iter() - .find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| { - liab_usdc_equivalent.is_negative() - && jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX) - }) - .ok_or_else(|| { - anyhow::anyhow!( - "mango account {}, has no liab tokens that are purchasable for USDC: {:?}", - pubkey, - tokens - ) - })? - .0; - - let max_liab_transfer = get_max_liab_transfer(liab_token_index, quote_token_index)?; - - let sig = mango_client - .token_liq_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer) - .context("sending liq_token_bankruptcy")?; - log::info!( - "Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}", - pubkey, - maint_health, - sig - ); - sig - } else if is_spot_liquidatable { - 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(mango_client, *asset_token_index, QUOTE_TOKEN_INDEX) - }) - .ok_or_else(|| { - anyhow::anyhow!( - "mango account {}, has no asset tokens that are sellable for USDC: {:?}", - pubkey, - tokens - ) - })? - .0; - let liab_token_index = tokens - .iter() - .find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| { - liab_usdc_equivalent.is_negative() - && jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX) - }) - .ok_or_else(|| { - anyhow::anyhow!( - "mango account {}, has no liab tokens that are purchasable for USDC: {:?}", - pubkey, - tokens - ) - })? - .0; - - let max_liab_transfer = get_max_liab_transfer(liab_token_index, asset_token_index) - .context("getting max_liab_transfer")?; - - // - // TODO: log liqor's assets in UI form - // TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side - // - let sig = mango_client - .token_liq_with_token( - (pubkey, &account), - asset_token_index, - liab_token_index, - max_liab_transfer, - ) - .context("sending liq_token_with_token")?; - log::info!( - "Liquidated token with token for {}, maint_health was {}, tx sig {:?}", - pubkey, - maint_health, - sig - ); - sig - } else { - anyhow::bail!( - "Don't know what to do with liquidatable account {}, maint_health was {}", - pubkey, - maint_health - ); - }; + let txsig = LiquidateHelper { + client: mango_client, + account_fetcher, + pubkey, + liqee: &account, + health_cache: &health_cache, + maint_health, + liqor_min_health_ratio, + } + .send_liq_tx()?; let slot = account_fetcher.transaction_max_slot(&[txsig])?; if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot( diff --git a/ts/client/src/scripts/mb-liqtest-make-candidates.ts b/ts/client/src/scripts/mb-liqtest-make-candidates.ts index f05febe3b..97ec6a545 100644 --- a/ts/client/src/scripts/mb-liqtest-make-candidates.ts +++ b/ts/client/src/scripts/mb-liqtest-make-candidates.ts @@ -22,6 +22,7 @@ const PRICES = { BTC: 20000.0, SOL: 0.04, USDC: 1, + MNGO: 0.04, }; const MAINNET_MINTS = new Map([ @@ -298,6 +299,109 @@ async function main() { } } + // borrows and positive perp pnl (but no position) + { + const name = 'LIQTEST, perp positive pnl'; + + console.log(`Creating mangoaccount...`); + let mangoAccount = await createMangoAccount(name); + console.log( + `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, + ); + + const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!); + const baseOracle = (await client.getStubOracle(group, baseMint))[0] + .publicKey; + const liabMint = new PublicKey(MAINNET_MINTS.get('USDC')!); + const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!); + const collateralOracle = group.banksMapByName.get('SOL')![0].oracle; + + await client.tokenDepositNative( + group, + mangoAccount, + collateralMint, + 100000, + ); // valued as $0.004 maint collateral + await mangoAccount.reload(client, group); + + try { + await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 10); + + // Spot-borrow more than the collateral is worth + await client.tokenWithdrawNative( + group, + mangoAccount, + liabMint, + -5000, + true, + ); + await mangoAccount.reload(client, group); + + // Execute two trades that leave the account with +$0.022 positive pnl + await client.stubOracleSet(group, baseOracle, PRICES['MNGO'] / 2); + await client.perpPlaceOrder( + group, + fundingAccount, + 'MNGO-PERP', + Side.ask, + 20, + 0.0011, // ui base quantity, 11 base lots, $0.022 + 0.022, // ui quote quantity + 4200, + PerpOrderType.limit, + 0, + 5, + ); + await client.perpPlaceOrder( + group, + mangoAccount, + 'MNGO-PERP', + Side.bid, + 20, + 0.0011, // ui base quantity, 11 base lots, $0.022 + 0.022, // ui quote quantity + 4200, + PerpOrderType.market, + 0, + 5, + ); + await client.perpConsumeAllEvents(group, 'MNGO-PERP'); + + await client.stubOracleSet(group, baseOracle, PRICES['MNGO']); + + await client.perpPlaceOrder( + group, + fundingAccount, + 'MNGO-PERP', + Side.bid, + 40, + 0.0011, // ui base quantity, 11 base lots, $0.044 + 0.044, // ui quote quantity + 4201, + PerpOrderType.limit, + 0, + 5, + ); + await client.perpPlaceOrder( + group, + mangoAccount, + 'MNGO-PERP', + Side.ask, + 40, + 0.0011, // ui base quantity, 11 base lots, $0.044 + 0.044, // ui quote quantity + 4201, + PerpOrderType.market, + 0, + 5, + ); + await client.perpConsumeAllEvents(group, 'MNGO-PERP'); + } finally { + await client.stubOracleSet(group, collateralOracle, PRICES['SOL']); + await client.stubOracleSet(group, baseOracle, PRICES['MNGO']); + } + } + process.exit(); } From 15d3a0cdfafc0fcba38ab5ca89c030a89b7350bd Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 26 Sep 2022 15:58:26 +0200 Subject: [PATCH 20/39] ts client: fix reading perp event queue --- ts/client/src/accounts/perp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index 41a9ce68d..ce00a0cfc 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -533,7 +533,7 @@ export class PerpEventQueue { this.head = header.head; this.count = header.count; this.seqNum = header.seqNum; - this.rawEvents = buf.slice(this.head, this.count).map((event) => { + this.rawEvents = buf.map((event) => { if (event.eventType === PerpEventQueue.FILL_EVENT_TYPE) { return (client.program as any)._coder.types.typeLayouts .get('FillEvent') From dd83aeb1561496e49b29b6063358dbb7c790bab3 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 27 Sep 2022 15:13:53 +0200 Subject: [PATCH 21/39] ts: ALTs integration Signed-off-by: microwavedcola1 --- package.json | 4 + ts/client/src/accounts/group.ts | 28 +++- ts/client/src/client.ts | 232 ++++++++++++++++++-------- ts/client/src/scripts/devnet-admin.ts | 80 ++++++++- ts/client/src/scripts/devnet-user.ts | 28 ++-- ts/client/src/utils.ts | 26 +++ ts/client/src/utils/rpc.ts | 35 ++-- yarn.lock | 163 ++++++++++-------- 8 files changed, 418 insertions(+), 178 deletions(-) diff --git a/package.json b/package.json index 9e46e2836..b6d292d67 100644 --- a/package.json +++ b/package.json @@ -64,10 +64,14 @@ "@project-serum/serum": "^0.13.65", "@pythnetwork/client": "^2.7.0", "@solana/spl-token": "^0.1.8", + "@solana/web3.js": "^1.63.1", "@switchboard-xyz/switchboard-v2": "^0.0.129", "big.js": "^6.1.1", "bs58": "^5.0.0" }, + "resolutions": { + "@project-serum/anchor/@solana/web3.js": "1.63.1" + }, "peerDependencies": { "@solana/spl-token-swap": "^0.2.0" }, diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index f78064c31..2973a5ed4 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -7,7 +7,11 @@ import { Orderbook, } from '@project-serum/serum'; import { parsePriceData, PriceData } from '@pythnetwork/client'; -import { AccountInfo, PublicKey } from '@solana/web3.js'; +import { + AccountInfo, + AddressLookupTableAccount, + PublicKey, +} from '@solana/web3.js'; import BN from 'bn.js'; import { MangoClient } from '../client'; import { SERUM3_PROGRAM_ID } from '../constants'; @@ -35,6 +39,7 @@ export class Group { insuranceVault: PublicKey; testing: number; version: number; + addressLookupTables: PublicKey[]; }, ): Group { return new Group( @@ -47,6 +52,8 @@ export class Group { obj.insuranceVault, obj.testing, obj.version, + obj.addressLookupTables, + [], // addressLookupTablesList new Map(), // banksMapByName new Map(), // banksMapByMint new Map(), // banksMapByTokenIndex @@ -70,6 +77,8 @@ export class Group { public insuranceVault: PublicKey, public testing: number, public version: number, + public addressLookupTables: PublicKey[], + public addressLookupTablesList: AddressLookupTableAccount[], public banksMapByName: Map, public banksMapByMint: Map, public banksMapByTokenIndex: Map, @@ -96,6 +105,7 @@ export class Group { // console.time('group.reload'); await Promise.all([ + this.reloadAlts(client), this.reloadBanks(client, ids).then(() => Promise.all([ this.reloadBankOraclePrices(client), @@ -113,6 +123,22 @@ export class Group { // console.timeEnd('group.reload'); } + public async reloadAlts(client: MangoClient) { + const alts = await Promise.all( + this.addressLookupTables + .filter((alt) => !alt.equals(PublicKey.default)) + .map((alt) => + client.program.provider.connection.getAddressLookupTable(alt), + ), + ); + this.addressLookupTablesList = alts.map((res, i) => { + if (!res || !res.value) { + throw new Error(`Error in getting ALT ${this.addressLookupTables[i]}`); + } + return res.value; + }); + } + public async reloadBanks(client: MangoClient, ids?: Id) { let banks: Bank[]; diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index a881ce8e5..32763a374 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -35,12 +35,12 @@ import { } from './accounts/mangoAccount'; import { StubOracle } from './accounts/oracle'; import { - PerpEventQueue, - PerpMarket, - PerpOrderType, - PerpOrderSide, FillEvent, OutEvent, + PerpEventQueue, + PerpMarket, + PerpOrderSide, + PerpOrderType, } from './accounts/perp'; import { generateSerum3MarketExternalVaultSignerAddress, @@ -553,7 +553,7 @@ export class MangoClient { perpCount?: number, perpOoCount?: number, ): Promise { - const transaction = await this.program.methods + const ix = await this.program.methods .accountCreate( accountNumber ?? 0, tokenCount ?? 8, @@ -567,11 +567,12 @@ export class MangoClient { owner: (this.program.provider as AnchorProvider).wallet.publicKey, payer: (this.program.provider as AnchorProvider).wallet.publicKey, }) - .transaction(); + .instruction(); return await sendTransaction( this.program.provider as AnchorProvider, - transaction, + [ix], + [], { postSendTxCallback: this.postSendTxCallback, }, @@ -629,18 +630,19 @@ export class MangoClient { name?: string, delegate?: PublicKey, ): Promise { - const transaction = await this.program.methods + const ix = await this.program.methods .accountEdit(name ?? null, delegate ?? null) .accounts({ group: group.publicKey, account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, }) - .transaction(); + .instruction(); return await sendTransaction( this.program.provider as AnchorProvider, - transaction, + [ix], + [], { postSendTxCallback: this.postSendTxCallback, }, @@ -731,7 +733,7 @@ export class MangoClient { group: Group, mangoAccount: MangoAccount, ): Promise { - const transaction = await this.program.methods + const ix = await this.program.methods .accountClose() .accounts({ group: group.publicKey, @@ -739,11 +741,12 @@ export class MangoClient { owner: (this.program.provider as AnchorProvider).wallet.publicKey, solDestination: mangoAccount.owner, }) - .transaction(); + .instruction(); return await sendTransaction( this.program.provider as AnchorProvider, - transaction, + [ix], + [], { postSendTxCallback: this.postSendTxCallback, }, @@ -866,7 +869,7 @@ export class MangoClient { [], ); - const transaction = await this.program.methods + const ix = await this.program.methods .tokenDeposit(new BN(nativeAmount)) .accounts({ group: group.publicKey, @@ -883,14 +886,12 @@ export class MangoClient { ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), ), ) - .preInstructions(preInstructions) - .postInstructions(postInstructions) - .signers(additionalSigners) - .transaction(); + .instruction(); return await sendTransaction( this.program.provider as AnchorProvider, - transaction, + [...preInstructions, ix, ...postInstructions], + group.addressLookupTablesList, { additionalSigners, postSendTxCallback: this.postSendTxCallback, @@ -961,7 +962,7 @@ export class MangoClient { [], ); - const tx = await this.program.methods + const ix = await this.program.methods .tokenWithdraw(new BN(nativeAmount), allowBorrow) .accounts({ group: group.publicKey, @@ -978,13 +979,16 @@ export class MangoClient { ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), ), ) - .preInstructions(preInstructions) - .postInstructions(postInstructions) - .transaction(); + .instruction(); - return await sendTransaction(this.program.provider as AnchorProvider, tx, { - postSendTxCallback: this.postSendTxCallback, - }); + return await sendTransaction( + this.program.provider as AnchorProvider, + [...preInstructions, ix, ...postInstructions], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); } // Serum @@ -1192,7 +1196,7 @@ export class MangoClient { } })(); - const tx = await this.program.methods + const ix = await this.program.methods .serum3PlaceOrder( side, limitPrice, @@ -1228,11 +1232,16 @@ export class MangoClient { ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), ), ) - .transaction(); + .instruction(); - return await sendTransaction(this.program.provider as AnchorProvider, tx, { - postSendTxCallback: this.postSendTxCallback, - }); + return await sendTransaction( + this.program.provider as AnchorProvider, + [ix], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); } async serum3CancelAllorders( @@ -1249,7 +1258,7 @@ export class MangoClient { externalMarketPk.toBase58(), )!; - const tx = await this.program.methods + const ix = await this.program.methods .serum3CancelAllOrders(limit) .accounts({ group: group.publicKey, @@ -1264,11 +1273,16 @@ export class MangoClient { marketAsks: serum3MarketExternal.asksAddress, marketEventQueue: serum3MarketExternal.decoded.eventQueue, }) - .transaction(); + .instruction(); - return await sendTransaction(this.program.provider as AnchorProvider, tx, { - postSendTxCallback: this.postSendTxCallback, - }); + return await sendTransaction( + this.program.provider as AnchorProvider, + [ix], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); } async serum3SettleFunds( @@ -1289,7 +1303,7 @@ export class MangoClient { serum3MarketExternal, ); - const tx = await this.program.methods + const ix = await this.program.methods .serum3SettleFunds() .accounts({ group: group.publicKey, @@ -1312,11 +1326,16 @@ export class MangoClient { baseVault: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) .vault, }) - .transaction(); + .instruction(); - return await sendTransaction(this.program.provider as AnchorProvider, tx, { - postSendTxCallback: this.postSendTxCallback, - }); + return await sendTransaction( + this.program.provider as AnchorProvider, + [ix], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); } async serum3CancelOrder( @@ -1334,7 +1353,7 @@ export class MangoClient { externalMarketPk.toBase58(), )!; - const tx = await this.program.methods + const ix = await this.program.methods .serum3CancelOrder(side, orderId) .accounts({ group: group.publicKey, @@ -1348,11 +1367,16 @@ export class MangoClient { marketAsks: serum3MarketExternal.asksAddress, marketEventQueue: serum3MarketExternal.decoded.eventQueue, }) - .transaction(); + .instruction(); - return await sendTransaction(this.program.provider as AnchorProvider, tx, { - postSendTxCallback: this.postSendTxCallback, - }); + return await sendTransaction( + this.program.provider as AnchorProvider, + [ix], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); } /// perps @@ -1409,7 +1433,7 @@ export class MangoClient { new BN(impactQuantity), groupInsuranceFund, trustedMarket, - feePenalty + feePenalty, ) .accounts({ group: group.publicKey, @@ -1506,7 +1530,7 @@ export class MangoClient { new BN(impactQuantity), groupInsuranceFund, trustedMarket, - feePenalty + feePenalty, ) .accounts({ group: group.publicKey, @@ -1609,7 +1633,7 @@ export class MangoClient { [group.getFirstBankByTokenIndex(0)], [perpMarket], ); - return await this.program.methods + const ix = await this.program.methods .perpPlaceOrder( side, perpMarket.uiPriceToLots(price), @@ -1638,7 +1662,16 @@ export class MangoClient { ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), ), ) - .rpc(); + .instruction(); + + return await sendTransaction( + this.program.provider as AnchorProvider, + [ix], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); } async perpCancelAllOrders( @@ -1648,7 +1681,7 @@ export class MangoClient { limit: number, ): Promise { const perpMarket = group.perpMarketsMap.get(perpMarketName)!; - return await this.program.methods + const ix = await this.program.methods .perpCancelAllOrders(limit) .accounts({ group: group.publicKey, @@ -1658,7 +1691,16 @@ export class MangoClient { bids: perpMarket.bids, owner: (this.program.provider as AnchorProvider).wallet.publicKey, }) - .rpc(); + .instruction(); + + return await sendTransaction( + this.program.provider as AnchorProvider, + [ix], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); } async perpConsumeEvents( @@ -1691,18 +1733,20 @@ export class MangoClient { const limit = 8; const perpMarket = group.perpMarketsMap.get(perpMarketName)!; const eventQueue = await perpMarket.loadEventQueue(this); - let unconsumedEvents = eventQueue.getUnconsumedEvents(); + const unconsumedEvents = eventQueue.getUnconsumedEvents(); while (unconsumedEvents.length > 0) { const events = unconsumedEvents.splice(0, limit); const accounts = events .map((ev) => { switch (ev.eventType) { - case PerpEventQueue.FILL_EVENT_TYPE: + case PerpEventQueue.FILL_EVENT_TYPE: { const fill = ev; return [fill.maker, fill.taker]; - case PerpEventQueue.OUT_EVENT_TYPE: + } + case PerpEventQueue.OUT_EVENT_TYPE: { const out = ev; return [out.owner]; + } case PerpEventQueue.LIQUIDATE_EVENT_TYPE: return []; default: @@ -1873,21 +1917,19 @@ export class MangoClient { ]) .instruction(); - const tx = new Transaction(); - for (const ix of preInstructions) { - tx.add(ix); - } - tx.add(flashLoanBeginIx); - for (const ix of userDefinedInstructions.filter( - (ix) => ix.keys.length > 2, - )) { - tx.add(ix); - } - tx.add(flashLoanEndIx); - - return await sendTransaction(this.program.provider as AnchorProvider, tx, { - postSendTxCallback: this.postSendTxCallback, - }); + return await sendTransaction( + this.program.provider as AnchorProvider, + [ + ...preInstructions, + flashLoanBeginIx, + ...userDefinedInstructions.filter((ix) => ix.keys.length > 2), + flashLoanEndIx, + ], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); } async updateIndexAndRate(group: Group, mintPk: PublicKey) { @@ -1947,7 +1989,7 @@ export class MangoClient { } as AccountMeta), ); - await this.program.methods + const ix = await this.program.methods .liqTokenWithToken(assetBank.tokenIndex, liabBank.tokenIndex, { val: I80F48.fromNumber(maxLiabTransfer).getData(), }) @@ -1958,6 +2000,52 @@ export class MangoClient { liqorOwner: liqor.owner, }) .remainingAccounts(parsedHealthAccounts) + .instruction(); + + return await sendTransaction( + this.program.provider as AnchorProvider, + [ix], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); + } + + async altSet(group: Group, addressLookupTable: PublicKey, index: number) { + const ix = await this.program.methods + .altSet(index) + .accounts({ + group: group.publicKey, + admin: (this.program.provider as AnchorProvider).wallet.publicKey, + addressLookupTable, + }) + .instruction(); + + return await sendTransaction( + this.program.provider as AnchorProvider, + [ix], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); + } + + async altExtend( + group: Group, + addressLookupTable: PublicKey, + index: number, + pks: PublicKey[], + ) { + return await this.program.methods + .altExtend(index, pks) + .accounts({ + group: group.publicKey, + admin: (this.program.provider as AnchorProvider).wallet.publicKey, + payer: (this.program.provider as AnchorProvider).wallet.publicKey, + addressLookupTable, + }) .rpc(); } @@ -2141,7 +2229,7 @@ export class MangoClient { ...mintInfos.map((mintInfo) => mintInfo.oracle), ); - let perpIndices: number[] = []; + const perpIndices: number[] = []; for (const mangoAccount of mangoAccounts) { perpIndices.push( ...mangoAccount.perps diff --git a/ts/client/src/scripts/devnet-admin.ts b/ts/client/src/scripts/devnet-admin.ts index f0b0369d6..f15a98afb 100644 --- a/ts/client/src/scripts/devnet-admin.ts +++ b/ts/client/src/scripts/devnet-admin.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'; // // An example for admins based on high level api i.e. the client @@ -508,6 +514,78 @@ async function main() { } } + if ( + // true + 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}?cluster=devnet`, + ); + + console.log(`ALT: set at index 0 for group...`); + sig = await client.altSet( + group, + new PublicKey('EmN5RjHUFsoag7tZ2AyBL2N8JrhV7nLMKgNbpCfzC81D'), + 0, + ); + console.log(`...https://explorer.solana.com/tx/${sig}?cluster=devnet`); + + // Extend using a mango v4 program ix + // Throws > Instruction references an unknown account 11111111111111111111111111111111 atm + // + console.log( + `ALT: extending using mango v4 program with bank publick keys and oracles`, + ); + // let sig = await client.altExtend( + // group, + // new PublicKey('EmN5RjHUFsoag7tZ2AyBL2N8JrhV7nLMKgNbpCfzC81D'), + // 0, + // Array.from(group.banksMapByMint.values()) + // .flat() + // .map((bank) => [bank.publicKey, bank.oracle]) + // .flat(), + // ); + // console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); + + // TODO decide on what keys should go in + console.log(`ALT: extending manually with bank publick keys and oracles`); + const extendIx = AddressLookupTableProgram.extendLookupTable({ + lookupTable: createIx[1], + payer: admin.publicKey, + authority: admin.publicKey, + addresses: Array.from(group.banksMapByMint.values()) + .flat() + .map((bank) => [bank.publicKey, bank.oracle]) + .flat(), + }); + const extendTx = await buildVersionedTx( + client.program.provider as AnchorProvider, + [extendIx], + ); + sig = await client.program.provider.connection.sendTransaction(extendTx); + console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); + } catch (error) { + console.log(error); + } + } + + try { + } catch (error) { + console.log(error); + } + process.exit(); } diff --git a/ts/client/src/scripts/devnet-user.ts b/ts/client/src/scripts/devnet-user.ts index 8b3499ff2..4c4e5b25e 100644 --- a/ts/client/src/scripts/devnet-user.ts +++ b/ts/client/src/scripts/devnet-user.ts @@ -2,7 +2,7 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; import { HealthType } from '../accounts/mangoAccount'; -import { BookSide, PerpOrderType, Side } from '../accounts/perp'; +import { BookSide, PerpOrderSide, PerpOrderType } from '../accounts/perp'; import { Serum3OrderType, Serum3SelfTradeBehavior, @@ -117,7 +117,7 @@ async function main() { } // deposit and withdraw - if (false) { + if (true) { try { console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`); await client.tokenDeposit( @@ -169,7 +169,7 @@ async function main() { } } - if (false) { + if (true) { // serum3 const serum3Market = group.serum3MarketsMapByExternal.get( DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!, @@ -282,7 +282,7 @@ async function main() { ); } - if (false) { + if (true) { // serum3 market const serum3Market = group.serum3MarketsMapByExternal.get( DEVNET_SERUM3_MARKETS.get('BTC/USDC')!.toBase58(), @@ -290,7 +290,7 @@ async function main() { console.log(await serum3Market?.logOb(client, group)); } - if (false) { + if (true) { await mangoAccount.reload(client, group); console.log( '...mangoAccount.getEquity() ' + @@ -333,7 +333,7 @@ async function main() { ); } - if (false) { + if (true) { const asks = await group.loadSerum3AsksForMarket( client, DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, @@ -434,7 +434,7 @@ async function main() { group, mangoAccount, 'BTC-PERP', - Side.bid, + PerpOrderSide.bid, price, baseQty, quoteQty, @@ -467,7 +467,7 @@ async function main() { group, mangoAccount, 'BTC-PERP', - Side.bid, + PerpOrderSide.bid, price, baseQty, quoteQty, @@ -496,7 +496,7 @@ async function main() { group, mangoAccount, 'BTC-PERP', - Side.ask, + PerpOrderSide.ask, price, baseQty, quoteQty, @@ -526,7 +526,7 @@ async function main() { group, mangoAccount, 'BTC-PERP', - Side.ask, + PerpOrderSide.ask, price, baseQty, quoteQty, @@ -554,7 +554,7 @@ async function main() { // group, // mangoAccount, // 'BTC-PERP', - // Side.bid, + // PerpOrderSide.bid, // price, // 0.01, // price * 0.01, @@ -575,7 +575,7 @@ async function main() { // group, // mangoAccount, // 'BTC-PERP', - // Side.ask, + // PerpOrderSide.ask, // price, // 0.01, // price * 0.011, @@ -593,7 +593,7 @@ async function main() { // // sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); // // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - const perpMarket = group.perpMarketsMap.get('BTC-PERP'); + const perpMarket = group.perpMarketsMap.get('BTC-PERP')!; const bids: BookSide = await perpMarket?.loadBids(client)!; console.log(`bids - ${Array.from(bids.items())}`); @@ -601,7 +601,7 @@ async function main() { console.log(`asks - ${Array.from(asks.items())}`); await perpMarket?.loadEventQueue(client)!; - const fr = await perpMarket?.getCurrentFundingRate( + const fr = perpMarket?.getCurrentFundingRate( await perpMarket.loadBids(client), await perpMarket.loadAsks(client), ); diff --git a/ts/client/src/utils.ts b/ts/client/src/utils.ts index 1f023e8fd..d6f616b74 100644 --- a/ts/client/src/utils.ts +++ b/ts/client/src/utils.ts @@ -1,12 +1,17 @@ +import { AnchorProvider } from '@project-serum/anchor'; import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, } from '@solana/spl-token'; import { AccountMeta, + AddressLookupTableAccount, + MessageV0, PublicKey, + Signer, SystemProgram, TransactionInstruction, + VersionedTransaction, } from '@solana/web3.js'; import BN from 'bn.js'; import { Bank, QUOTE_DECIMALS } from './accounts/bank'; @@ -188,3 +193,24 @@ export function toU64(amount: number, decimals: number): BN { export function nativeI80F48ToUi(amount: I80F48, decimals: number): I80F48 { return amount.div(I80F48.fromNumber(Math.pow(10, decimals))); } + +export async function buildVersionedTx( + provider: AnchorProvider, + ix: TransactionInstruction[], + additionalSigners: Signer[] = [], + alts: AddressLookupTableAccount[] = [], +): Promise { + const message = MessageV0.compile({ + payerKey: (provider as AnchorProvider).wallet.publicKey, + instructions: ix, + recentBlockhash: (await provider.connection.getLatestBlockhash()).blockhash, + addressLookupTableAccounts: alts, + }); + const vTx = new VersionedTransaction(message); + // TODO: remove use of any when possible in future + vTx.sign([ + ((provider as AnchorProvider).wallet as any).payer as Signer, + ...additionalSigners, + ]); + return vTx; +} diff --git a/ts/client/src/utils/rpc.ts b/ts/client/src/utils/rpc.ts index 4d1a03ab0..e2b2ad074 100644 --- a/ts/client/src/utils/rpc.ts +++ b/ts/client/src/utils/rpc.ts @@ -1,27 +1,28 @@ import { AnchorProvider } from '@project-serum/anchor'; -import { Transaction } from '@solana/web3.js'; +import { + AddressLookupTableAccount, + TransactionInstruction, +} from '@solana/web3.js'; +import { buildVersionedTx } from '../utils'; export async function sendTransaction( provider: AnchorProvider, - transaction: Transaction, + ixs: TransactionInstruction[], + alts: AddressLookupTableAccount[], opts: any = {}, ) { const connection = provider.connection; - const payer = provider.wallet; const latestBlockhash = await connection.getLatestBlockhash( opts.preflightCommitment, ); - transaction.recentBlockhash = latestBlockhash.blockhash; - transaction.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight; - transaction.feePayer = payer.publicKey; - if (opts.additionalSigners?.length > 0) { - transaction.partialSign(...opts.additionalSigners); - } + const tx = await buildVersionedTx( + provider, + ixs, + opts.additionalSigners, + alts, + ); - await payer.signTransaction(transaction); - const rawTransaction = transaction.serialize(); - - const signature = await connection.sendRawTransaction(rawTransaction, { + const signature = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true, }); @@ -35,16 +36,16 @@ export async function sendTransaction( let status: any; if ( - transaction.recentBlockhash != null && - transaction.lastValidBlockHeight != null + latestBlockhash.blockhash != null && + latestBlockhash.lastValidBlockHeight != null ) { console.log('confirming via blockhash'); status = ( await connection.confirmTransaction( { signature: signature, - blockhash: transaction.recentBlockhash, - lastValidBlockHeight: transaction.lastValidBlockHeight, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, }, 'processed', ) diff --git a/yarn.lock b/yarn.lock index dd24b794a..65f1681d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,13 +23,20 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2": +"@babel/runtime@^7.10.5": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" + integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== + dependencies: + regenerator-runtime "^0.13.4" + "@cykura/sdk-core@npm:@jup-ag/cykura-sdk-core@0.1.8", "@jup-ag/cykura-sdk-core@0.1.8": version "0.1.8" resolved "https://registry.npmjs.org/@jup-ag/cykura-sdk-core/-/cykura-sdk-core-0.1.8.tgz" @@ -192,6 +199,21 @@ resolved "https://registry.npmjs.org/@mercurial-finance/optimist/-/optimist-0.1.4.tgz" integrity sha512-m8QuyPx9j7fGd2grw0mD5WcYtBb8l7+OQI5aHdeIlxPg3QoPrbSdCHyFOuipYbvB0EY5YDbOmyeFwiTcBkBBSw== +"@noble/ed25519@^1.7.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.1.tgz#6899660f6fbb97798a6fbd227227c4589a454724" + integrity sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw== + +"@noble/hashes@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" + integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== + +"@noble/secp256k1@^1.6.3": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.0.tgz#d15357f7c227e751d90aa06b05a0e5cf993ba8c1" + integrity sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -514,7 +536,7 @@ "@solana/buffer-layout@^4.0.0": version "4.0.0" - resolved "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz#75b1b11adc487234821c81dfae3119b73a5fd734" integrity sha512-lR0EMP2HC3+Mxwd4YcnZb0smnaDw7Bl2IQWZiTevRH5ZZBZn6VRWn3/92E3qdU4SSImJkA6IDHawOHAnx/qUvQ== dependencies: buffer "~6.0.3" @@ -583,7 +605,28 @@ superstruct "^0.14.2" tweetnacl "^1.0.0" -"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.36.0": +"@solana/web3.js@1.63.1", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.63.1": + version "1.63.1" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.63.1.tgz#88a19a17f5f4aada73ad70a94044c1067cab2b4d" + integrity sha512-wgEdGVK5FTS2zENxbcGSvKpGZ0jDS6BUdGu8Gn6ns0CzgJkK83u4ip3THSnBPEQ5i/jrqukg998BwV1H67+qiQ== + dependencies: + "@babel/runtime" "^7.12.5" + "@noble/ed25519" "^1.7.0" + "@noble/hashes" "^1.1.2" + "@noble/secp256k1" "^1.6.3" + "@solana/buffer-layout" "^4.0.0" + bigint-buffer "^1.1.5" + bn.js "^5.0.0" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.1" + fast-stable-stringify "^1.0.0" + jayson "^3.4.4" + node-fetch "2" + rpc-websockets "^7.5.0" + superstruct "^0.14.2" + +"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0": version "1.51.0" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.51.0.tgz#51b28b5332f1f03ea25bea6c229869e33aebc507" integrity sha512-kf2xHKYETKiIY4DCt8Os7VDPoY5oyOMJ3UWRcLeOVEFXwiv2ClNmSg0EG3BqV3I4TwOojGtmVqk0ubCkUnpmfg== @@ -702,20 +745,11 @@ "@types/connect@^3.4.33": version "3.4.35" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== dependencies: "@types/node" "*" -"@types/express-serve-static-core@^4.17.9": - version "4.17.30" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz#0f2f99617fa8f9696170c46152ccf7500b34ac04" - integrity sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" @@ -726,11 +760,6 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/lodash@^4.14.159": - version "4.14.182" - resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz" - integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== - "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -742,9 +771,9 @@ integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg== "@types/node@*": - version "18.7.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.1.tgz#352bee64f93117d867d05f7406642a52685cbca6" - integrity sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ== + version "18.7.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.22.tgz#76f7401362ad63d9d7eefa7dcdfa5fcd9baddff3" + integrity sha512-TsmoXYd4zrkkKjJB0URF/mTIKPl+kVcbqClB2F/ykU7vil1BfWZVndOnpEIozPv4fURD28gyPFeIkW2G+KXOvw== "@types/node@>=13.7.0": version "18.7.6" @@ -768,16 +797,6 @@ dependencies: "@types/retry" "*" -"@types/qs@*": - version "6.9.7" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" - integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== - -"@types/range-parser@*": - version "1.2.4" - resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz" - integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== - "@types/retry@*", "@types/retry@^0.12.2": version "0.12.2" resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz" @@ -785,7 +804,7 @@ "@types/ws@^7.4.4": version "7.4.7" - resolved "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== dependencies: "@types/node" "*" @@ -888,7 +907,7 @@ JSONStream@^1.3.5: version "1.3.5" - resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== dependencies: jsonparse "^1.2.0" @@ -1041,7 +1060,7 @@ base-x@^4.0.0: base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== big.js@^5.2.2: @@ -1061,7 +1080,7 @@ big.js@^6.2.0: bigint-buffer@^1.1.5: version "1.1.5" - resolved "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz" + resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== dependencies: bindings "^1.3.0" @@ -1078,7 +1097,7 @@ binary-extensions@^2.0.0: bindings@^1.3.0: version "1.5.0" - resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== dependencies: file-uri-to-path "1.0.0" @@ -1120,7 +1139,7 @@ borsh@^0.4.0: borsh@^0.7.0: version "0.7.0" - resolved "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz" + resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a" integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== dependencies: bn.js "^5.2.0" @@ -1161,7 +1180,7 @@ browser-stdout@1.3.1: bs58@^4.0.0, bs58@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== dependencies: base-x "^3.0.2" @@ -1185,7 +1204,7 @@ buffer-layout@^1.2.0, buffer-layout@^1.2.2: buffer@6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.1.tgz#3cbea8c1463e5a0779e30b66d4c88c6ffa182ac2" integrity sha512-rVAXBwEcEoYtxnHSO5iWyhzV/O1WMtkUYWlfdLS7FjU4PnSJJHEfHXi/uHPI5EwltmOA794gN3bm3/pzuctWjQ== dependencies: base64-js "^1.3.1" @@ -1193,7 +1212,7 @@ buffer@6.0.1: buffer@6.0.3, buffer@^6.0.1, buffer@~6.0.3: version "6.0.3" - resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: base64-js "^1.3.1" @@ -1209,7 +1228,7 @@ buffer@^5.4.3: bufferutil@^4.0.1: version "4.0.6" - resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.6.tgz#ebd6c67c7922a0e902f053e5d8be5ec850e48433" integrity sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw== dependencies: node-gyp-build "^4.3.0" @@ -1332,7 +1351,7 @@ color-name@~1.1.4: commander@^2.20.3: version "2.20.3" - resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== concat-map@0.0.1: @@ -1434,7 +1453,7 @@ define-properties@^1.1.3, define-properties@^1.1.4: delay@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== diff@5.0.0: @@ -1559,12 +1578,12 @@ es6-object-assign@^1.1.0: es6-promise@^4.0.3: version "4.2.8" - resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== es6-promisify@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== dependencies: es6-promise "^4.0.3" @@ -1730,7 +1749,7 @@ event-stream@=3.3.4: eventemitter3@^4.0.7: version "4.0.7" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== execa@5.1.1: @@ -1750,7 +1769,7 @@ execa@5.1.1: eyes@^0.1.8: version "0.1.8" - resolved "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: @@ -1781,7 +1800,7 @@ fast-levenshtein@^2.0.6: fast-stable-stringify@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313" integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== fastq@^1.6.0: @@ -1808,7 +1827,7 @@ file-entry-cache@^6.0.1: file-uri-to-path@1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== fill-range@^7.0.1: @@ -2066,7 +2085,7 @@ human-signals@^2.1.0: ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^4.0.6: @@ -2274,17 +2293,15 @@ isexe@^2.0.0: isomorphic-ws@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== jayson@^3.4.4: - version "3.6.6" - resolved "https://registry.npmjs.org/jayson/-/jayson-3.6.6.tgz" - integrity sha512-f71uvrAWTtrwoww6MKcl9phQTC+56AopLyEenWvKVAIMz+q0oVGj6tenLZ7Z6UiPBkJtKLj4kt0tACllFQruGQ== + version "3.7.0" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-3.7.0.tgz#b735b12d06d348639ae8230d7a1e2916cb078f25" + integrity sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ== dependencies: "@types/connect" "^3.4.33" - "@types/express-serve-static-core" "^4.17.9" - "@types/lodash" "^4.14.159" "@types/node" "^12.12.54" "@types/ws" "^7.4.4" JSONStream "^1.3.5" @@ -2361,7 +2378,7 @@ json-stable-stringify-without-jsonify@^1.0.1: json-stringify-safe@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== json5@^1.0.1: @@ -2378,7 +2395,7 @@ jsonc-parser@^3.0.0: jsonparse@^1.2.0: version "1.3.1" - resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== lazy-ass@1.6.0: @@ -2418,7 +2435,7 @@ lodash.truncate@^4.4.2: lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-symbols@4.1.0: @@ -2616,7 +2633,7 @@ node-domexception@^1.0.0: node-fetch@2, node-fetch@2.6.7: version "2.6.7" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" @@ -2846,7 +2863,7 @@ readdirp@~3.6.0: regenerator-runtime@^0.13.4: version "0.13.9" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== regexp.prototype.flags@^1.4.3: @@ -2902,7 +2919,7 @@ rimraf@^3.0.2: rpc-websockets@^7.4.2, rpc-websockets@^7.5.0: version "7.5.0" - resolved "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.5.0.tgz" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.0.tgz#bbeb87572e66703ff151e50af1658f98098e2748" integrity sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ== dependencies: "@babel/runtime" "^7.17.2" @@ -3108,7 +3125,7 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1. superstruct@^0.14.2: version "0.14.2" - resolved "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b" integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ== superstruct@^0.15.2, superstruct@^0.15.4: @@ -3150,7 +3167,7 @@ table@^6.0.9: text-encoding-utf-8@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== text-table@^0.2.0: @@ -3160,7 +3177,7 @@ text-table@^0.2.0: through@2, "through@>=2.2.7 <3", through@~2.3, through@~2.3.1: version "2.3.8" - resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== tiny-invariant@^1.1.0, tiny-invariant@^1.2.0, tiny-invariant@~1.2.0: @@ -3187,7 +3204,7 @@ toml@^3.0.0: tr46@~0.0.3: version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== traverse-chain@~0.1.0: @@ -3314,7 +3331,7 @@ uri-js@^4.2.2: utf-8-validate@^5.0.2: version "5.0.9" - resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.9.tgz#ba16a822fbeedff1a58918f2a6a6b36387493ea3" integrity sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q== dependencies: node-gyp-build "^4.3.0" @@ -3333,7 +3350,7 @@ util@^0.12.0: uuid@^8.3.2: version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-compile-cache@^2.0.3: @@ -3369,7 +3386,7 @@ web-streams-polyfill@^3.0.3: webidl-conversions@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== webidl-conversions@^5.0.0: @@ -3388,7 +3405,7 @@ whatwg-url-without-unicode@8.0.0-3: whatwg-url@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== dependencies: tr46 "~0.0.3" @@ -3454,9 +3471,9 @@ ws@^7.4.5: integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== ws@^8.5.0: - version "8.8.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" - integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== + version "8.9.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e" + integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg== y18n@^5.0.5: version "5.0.8" From 77abfd0529f85a4fb1d975566c58a5f60a663737 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 27 Sep 2022 11:10:28 +0200 Subject: [PATCH 22/39] Perp liq: Fix base position liquidation amount computation It was wrong for quote weights != 1. --- .../mango-v4/src/instructions/perp_liq_base_position.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/programs/mango-v4/src/instructions/perp_liq_base_position.rs b/programs/mango-v4/src/instructions/perp_liq_base_position.rs index 59cf3d1b7..70feab58b 100644 --- a/programs/mango-v4/src/instructions/perp_liq_base_position.rs +++ b/programs/mango-v4/src/instructions/perp_liq_base_position.rs @@ -112,7 +112,8 @@ pub fn perp_liq_base_position( // and increased by `base * price * (1 - liq_fee) * quote_init_asset_weight` let quote_asset_weight = I80F48::ONE; let health_per_lot = cm!(price_per_lot - * (quote_asset_weight - perp_market.init_asset_weight - perp_market.liquidation_fee)); + * (quote_asset_weight * (I80F48::ONE - perp_market.liquidation_fee) + - perp_market.init_asset_weight)); // number of lots to transfer to bring health to zero, rounded up let base_transfer_for_zero: i64 = cm!(-liqee_init_health / health_per_lot) @@ -141,7 +142,8 @@ pub fn perp_liq_base_position( // and reduced by `base * price * (1 + liq_fee) * quote_init_liab_weight` let quote_liab_weight = I80F48::ONE; let health_per_lot = cm!(price_per_lot - * (perp_market.init_liab_weight - quote_liab_weight + perp_market.liquidation_fee)); + * (perp_market.init_liab_weight * (I80F48::ONE + perp_market.liquidation_fee) + - quote_liab_weight)); // (negative) number of lots to transfer to bring health to zero, rounded away from zero let base_transfer_for_zero: i64 = cm!(liqee_init_health / health_per_lot) From 984695e8d085604e72ceb0cac5206b0ea0575268 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 27 Sep 2022 11:36:58 +0200 Subject: [PATCH 23/39] liquidator: Call liq_perp_bankruptcy --- client/src/client.rs | 65 ++++++++++++++++++---- liquidator/src/liquidate.rs | 78 ++++++++++++++++++++------- programs/mango-v4/src/state/health.rs | 2 +- 3 files changed, 117 insertions(+), 28 deletions(-) diff --git a/client/src/client.rs b/client/src/client.rs index 910c7df44..cff5ae093 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -315,8 +315,7 @@ impl MangoClient { pub fn derive_liquidation_health_check_remaining_account_metas( &self, liqee: &MangoAccountValue, - asset_token_index: TokenIndex, - liab_token_index: TokenIndex, + writable_banks: &[TokenIndex], ) -> anyhow::Result> { // figure out all the banks/oracles that need to be passed for the health check let mut banks = vec![]; @@ -331,7 +330,7 @@ impl MangoClient { for token_index in token_indexes { let mint_info = self.context.mint_info(token_index); - let writable_bank = token_index == asset_token_index || token_index == liab_token_index; + let writable_bank = writable_banks.iter().contains(&token_index); banks.push((mint_info.first_bank(), writable_bank)); oracles.push(mint_info.oracle); } @@ -928,8 +927,7 @@ impl MangoClient { let perp = self.context.perp(market_index); let health_remaining_ams = self - .context - .derive_health_check_remaining_account_metas(liqee.1, vec![], false) + .derive_liquidation_health_check_remaining_account_metas(liqee.1, &[]) .unwrap(); self.program() @@ -960,6 +958,57 @@ impl MangoClient { .map_err(prettify_client_error) } + pub fn perp_liq_bankruptcy( + &self, + liqee: (&Pubkey, &MangoAccountValue), + market_index: PerpMarketIndex, + max_liab_transfer: u64, + ) -> anyhow::Result { + let quote_token_index = 0; + let quote_info = self.context.token(quote_token_index); + + let group = account_fetcher_fetch_anchor_account::( + &*self.account_fetcher, + &self.context.group, + )?; + + let perp = self.context.perp(market_index); + + let health_remaining_ams = self + .derive_liquidation_health_check_remaining_account_metas(liqee.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::PerpLiqBankruptcy { + group: self.group(), + perp_market: perp.address, + liqor: self.mango_account_address, + liqor_owner: self.owner(), + liqee: *liqee.0, + quote_bank: quote_info.mint_info.first_bank(), + quote_vault: quote_info.mint_info.first_vault(), + 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) + } + // // Liquidation // @@ -974,8 +1023,7 @@ impl MangoClient { let health_remaining_ams = self .derive_liquidation_health_check_remaining_account_metas( liqee.1, - asset_token_index, - liab_token_index, + &[asset_token_index, liab_token_index], ) .unwrap(); @@ -1030,8 +1078,7 @@ impl MangoClient { let health_remaining_ams = self .derive_liquidation_health_check_remaining_account_metas( liqee.1, - quote_token_index, - liab_token_index, + &[quote_token_index, liab_token_index], ) .unwrap(); diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 7b09eddcb..0ed792764 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -148,7 +148,7 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn liq_perp_base_position(&self) -> anyhow::Result> { + fn perp_liq_base_position(&self) -> anyhow::Result> { let mut perp_base_positions = self .liqee .active_perp_positions() @@ -225,14 +225,14 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn settle_perp_pnl(&self) -> anyhow::Result> { + fn perp_settle_pnl(&self) -> anyhow::Result> { let spot_health = self.health_cache.spot_health(HealthType::Maint); let mut perp_settleable_pnl = self .liqee .active_perp_positions() - .map(|pp| { + .filter_map(|pp| { if pp.base_position_lots() != 0 { - return Ok(None); + return None; } let pnl = pp.quote_position_native(); let settleable_pnl = if pnl > 0 { @@ -240,12 +240,11 @@ impl<'a> LiquidateHelper<'a> { } else if pnl < 0 && spot_health > 0 { pnl.max(-spot_health) } else { - return Ok(None); + return None; }; - Ok(Some((pp.market_index, settleable_pnl))) + Some((pp.market_index, settleable_pnl)) }) - .filter_map_ok(|v| v) - .collect::>>()?; + .collect::>(); // sort by pnl, descending perp_settleable_pnl.sort_by(|a, b| b.1.cmp(&a.1)); @@ -293,6 +292,44 @@ impl<'a> LiquidateHelper<'a> { return Ok(None); } + fn perp_liq_bankruptcy(&self) -> anyhow::Result> { + if self.health_cache.has_liquidatable_assets() { + return Ok(None); + } + let mut perp_bankruptcies = self + .liqee + .active_perp_positions() + .filter_map(|pp| { + let quote = pp.quote_position_native(); + if quote >= 0 { + return None; + } + Some((pp.market_index, quote)) + }) + .collect::>(); + perp_bankruptcies.sort_by(|a, b| a.1.cmp(&b.1)); + + if perp_bankruptcies.is_empty() { + return Ok(None); + } + 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, + )?; + log::info!( + "Liquidated bankruptcy for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}", + self.pubkey, + perp_market_index, + self.maint_health, + sig + ); + Ok(Some(sig)) + } + fn tokens(&self) -> anyhow::Result> { let mut tokens = self .liqee @@ -354,7 +391,7 @@ impl<'a> LiquidateHelper<'a> { Ok(amount) } - fn liq_spot(&self) -> anyhow::Result> { + fn token_liq(&self) -> anyhow::Result> { if !self.health_cache.has_borrows() || self.health_cache.can_call_spot_bankruptcy() { return Ok(None); } @@ -417,7 +454,7 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn bankrupt_spot(&self) -> anyhow::Result> { + fn token_liq_bankruptcy(&self) -> anyhow::Result> { if !self.health_cache.can_call_spot_bankruptcy() { return Ok(None); } @@ -484,7 +521,7 @@ impl<'a> LiquidateHelper<'a> { return Ok(txsig); } - if let Some(txsig) = self.liq_perp_base_position()? { + if let Some(txsig) = self.perp_liq_base_position()? { return Ok(txsig); } @@ -493,21 +530,26 @@ 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.settle_perp_pnl()? { + if let Some(txsig) = self.perp_settle_pnl()? { return Ok(txsig); } - if let Some(txsig) = self.liq_spot()? { + if let Some(txsig) = self.token_liq()? { return Ok(txsig); } - // TODO: socialize unsettleable negative pnl - // if let Some(txsig) = self.bankrupt_perp()? { - // return Ok(txsig); - // } - if let Some(txsig) = self.bankrupt_spot()? { + // Socialize/insurance fund unsettleable negative pnl + if let Some(txsig) = self.perp_liq_bankruptcy()? { return Ok(txsig); } + + // Socialize/insurance fund unliquidatable borrows + if let Some(txsig) = self.token_liq_bankruptcy()? { + return Ok(txsig); + } + + // TODO: What about unliquidatable positive perp pnl? + anyhow::bail!( "Don't know what to do with liquidatable account {}, maint_health was {}", self.pubkey, diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index e73ed28be..31150525c 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -792,7 +792,7 @@ impl HealthCache { // can use perp_liq_base_position p.base != 0 // can use perp_settle_pnl - || p.quote > ONE_NATIVE_USDC_IN_USD + || 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 }); From 01a958cd22c35bacba6a3f0d4ccbd604db65cde2 Mon Sep 17 00:00:00 2001 From: Nicholas Clarke Date: Wed, 28 Sep 2022 23:04:33 -0700 Subject: [PATCH 24/39] Clarkeni/onchain interest (#244) * Onchain interest calculation * Fix to TokenBalanceLog for token_liq_bankruptcy (was previously using liqee liab position for liqor liab position). * Log cumulative interest when token position is deactivated. --- .../mango-v4/src/instructions/flash_loan.rs | 2 +- .../src/instructions/token_deposit.rs | 2 +- .../src/instructions/token_liq_bankruptcy.rs | 53 ++++++----- .../src/instructions/token_liq_with_token.rs | 24 ++--- .../src/instructions/token_withdraw.rs | 2 +- programs/mango-v4/src/logs.rs | 9 ++ programs/mango-v4/src/state/bank.rs | 37 +++++++- programs/mango-v4/src/state/mango_account.rs | 26 +++++- .../src/state/mango_account_components.rs | 20 +++- ts/client/src/mango_v4.ts | 92 ++++++++++++++++++- 10 files changed, 221 insertions(+), 46 deletions(-) diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 05de67c06..3c4328533 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -394,7 +394,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( // Deactivate inactive token accounts after health check for raw_token_index in deactivated_token_positions { - account.deactivate_token_position(raw_token_index); + account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key()); } Ok(()) diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index c0fab0c14..e040dd9dc 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -112,7 +112,7 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { // Deposits can deactivate a position if they cancel out a previous borrow. // if !position_is_active { - account.deactivate_token_position(raw_token_index); + account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key()); } emit!(DepositLog { diff --git a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs index d904428a1..ac1768538 100644 --- a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs @@ -170,7 +170,16 @@ pub fn token_liq_bankruptcy( let (liqor_quote, liqor_quote_raw_token_index, _) = liqor.ensure_token_position(QUOTE_TOKEN_INDEX)?; let liqor_quote_active = quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?; - let liqor_quote_indexed_position = liqor_quote.indexed_position; + + // liqor quote + emit!(TokenBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqor.key(), + token_index: QUOTE_TOKEN_INDEX, + indexed_position: liqor_quote.indexed_position.to_bits(), + deposit_index: quote_deposit_index.to_bits(), + borrow_index: quote_borrow_index.to_bits(), + }); // transfer liab from liqee to liqor let (liqor_liab, liqor_liab_raw_token_index, _) = @@ -178,6 +187,16 @@ pub fn token_liq_bankruptcy( let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?; + // liqor liab + emit!(TokenBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqor.key(), + token_index: liab_token_index, + indexed_position: liqor_liab.indexed_position.to_bits(), + deposit_index: liab_deposit_index.to_bits(), + borrow_index: liab_borrow_index.to_bits(), + }); + // Check liqor's health if !liqor.fixed.is_in_health_region() { let liqor_health = @@ -185,16 +204,6 @@ pub fn token_liq_bankruptcy( require!(liqor_health >= 0, MangoError::HealthMustBePositive); } - // liqor quote - emit!(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqor.key(), - token_index: QUOTE_TOKEN_INDEX, - indexed_position: liqor_quote_indexed_position.to_bits(), - deposit_index: quote_deposit_index.to_bits(), - borrow_index: quote_borrow_index.to_bits(), - }); - if loan_origination_fee.is_positive() { emit!(WithdrawLoanOriginationFeeLog { mango_group: ctx.accounts.group.key(), @@ -206,10 +215,16 @@ pub fn token_liq_bankruptcy( } if !liqor_quote_active { - liqor.deactivate_token_position(liqor_quote_raw_token_index); + liqor.deactivate_token_position_and_log( + liqor_quote_raw_token_index, + ctx.accounts.liqor.key(), + ); } if !liqor_liab_active { - liqor.deactivate_token_position(liqor_liab_raw_token_index); + liqor.deactivate_token_position_and_log( + liqor_liab_raw_token_index, + ctx.accounts.liqor.key(), + ); } } else { // For liab_token_index == QUOTE_TOKEN_INDEX: the insurance fund deposits directly into liqee, @@ -265,16 +280,6 @@ pub fn token_liq_bankruptcy( require_eq!(liqee_liab.indexed_position, I80F48::ZERO); } - // liqor liab - emit!(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqor.key(), - token_index: liab_token_index, - indexed_position: liqee_liab.indexed_position.to_bits(), - deposit_index: liab_deposit_index.to_bits(), - borrow_index: liab_borrow_index.to_bits(), - }); - // liqee liab emit!(TokenBalanceLog { mango_group: ctx.accounts.group.key(), @@ -297,7 +302,7 @@ pub fn token_liq_bankruptcy( .maybe_recover_from_being_liquidated(liqee_init_health); if !liqee_liab_active { - liqee.deactivate_token_position(liqee_raw_token_index); + liqee.deactivate_token_position_and_log(liqee_raw_token_index, ctx.accounts.liqee.key()); } emit!(LiquidateTokenBankruptcyLog { diff --git a/programs/mango-v4/src/instructions/token_liq_with_token.rs b/programs/mango-v4/src/instructions/token_liq_with_token.rs index 76fae6b82..55c8aa9c3 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -143,24 +143,24 @@ pub fn token_liq_with_token( // Apply the balance changes to the liqor and liqee accounts let liqee_liab_position = liqee.token_position_mut_by_raw_index(liqee_liab_raw_index); let liqee_liab_active = liab_bank.deposit_with_dusting(liqee_liab_position, liab_transfer)?; - let liqee_liab_position_indexed = liqee_liab_position.indexed_position; + let liqee_liab_indexed_position = liqee_liab_position.indexed_position; let (liqor_liab_position, liqor_liab_raw_index, _) = liqor.ensure_token_position(liab_token_index)?; let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?; - let liqor_liab_position_indexed = liqor_liab_position.indexed_position; + let liqor_liab_indexed_position = liqor_liab_position.indexed_position; let liqee_liab_native_after = liqee_liab_position.native(liab_bank); let (liqor_asset_position, liqor_asset_raw_index, _) = liqor.ensure_token_position(asset_token_index)?; let liqor_asset_active = asset_bank.deposit(liqor_asset_position, asset_transfer)?; - let liqor_asset_position_indexed = liqor_asset_position.indexed_position; + let liqor_asset_indexed_position = liqor_asset_position.indexed_position; let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index); let liqee_asset_active = asset_bank.withdraw_without_fee_with_dusting(liqee_asset_position, asset_transfer)?; - let liqee_asset_position_indexed = liqee_asset_position.indexed_position; + let liqee_asset_indexed_position = liqee_asset_position.indexed_position; let liqee_assets_native_after = liqee_asset_position.native(asset_bank); // Update the health cache @@ -184,7 +184,7 @@ pub fn token_liq_with_token( mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqee.key(), token_index: asset_token_index, - indexed_position: liqee_asset_position_indexed.to_bits(), + indexed_position: liqee_asset_indexed_position.to_bits(), deposit_index: asset_bank.deposit_index.to_bits(), borrow_index: asset_bank.borrow_index.to_bits(), }); @@ -193,7 +193,7 @@ pub fn token_liq_with_token( mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqee.key(), token_index: liab_token_index, - indexed_position: liqee_liab_position_indexed.to_bits(), + indexed_position: liqee_liab_indexed_position.to_bits(), deposit_index: liab_bank.deposit_index.to_bits(), borrow_index: liab_bank.borrow_index.to_bits(), }); @@ -202,7 +202,7 @@ pub fn token_liq_with_token( mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqor.key(), token_index: asset_token_index, - indexed_position: liqor_asset_position_indexed.to_bits(), + indexed_position: liqor_asset_indexed_position.to_bits(), deposit_index: asset_bank.deposit_index.to_bits(), borrow_index: asset_bank.borrow_index.to_bits(), }); @@ -211,7 +211,7 @@ pub fn token_liq_with_token( mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqor.key(), token_index: liab_token_index, - indexed_position: liqor_liab_position_indexed.to_bits(), + indexed_position: liqor_liab_indexed_position.to_bits(), deposit_index: liab_bank.deposit_index.to_bits(), borrow_index: liab_bank.borrow_index.to_bits(), }); @@ -228,16 +228,16 @@ pub fn token_liq_with_token( // Since we use a scanning account retriever, it's safe to deactivate inactive token positions if !liqee_asset_active { - liqee.deactivate_token_position(liqee_asset_raw_index); + liqee.deactivate_token_position_and_log(liqee_asset_raw_index, ctx.accounts.liqee.key()); } if !liqee_liab_active { - liqee.deactivate_token_position(liqee_liab_raw_index); + liqee.deactivate_token_position_and_log(liqee_liab_raw_index, ctx.accounts.liqee.key()); } if !liqor_asset_active { - liqor.deactivate_token_position(liqor_asset_raw_index); + liqor.deactivate_token_position_and_log(liqor_asset_raw_index, ctx.accounts.liqor.key()); } if !liqor_liab_active { - liqor.deactivate_token_position(liqor_liab_raw_index) + liqor.deactivate_token_position_and_log(liqor_liab_raw_index, ctx.accounts.liqor.key()) } // Check liqee health again diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index d2063ed6f..479ce08ca 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -152,7 +152,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo // deactivated. // if !position_is_active { - account.deactivate_token_position(raw_token_index); + account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key()); } emit!(WithdrawLog { diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index ef2be05be..9af48dfc0 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -207,3 +207,12 @@ pub struct LiquidateTokenBankruptcyLog { pub insurance_transfer: i128, pub socialized_loss: i128, } + +#[event] +pub struct DeactivateTokenPositionLog { + pub mango_group: Pubkey, + pub mango_account: Pubkey, + pub token_index: u16, + pub cumulative_deposit_interest: f32, + pub cumulative_borrow_interest: f32, +} diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index c28c80101..b2a47275f 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -210,6 +210,7 @@ impl Bank { ) -> Result { require_gte!(native_amount, 0); let native_position = position.native(self); + let opening_indexed_position = position.indexed_position; // Adding DELTA to amount/index helps because (amount/index)*index <= amount, but // we want to ensure that users can withdraw the same amount they have deposited, so @@ -235,12 +236,14 @@ impl Bank { // pay back borrows only, leaving a negative position cm!(self.indexed_borrows -= indexed_change); position.indexed_position = new_indexed_value; + self.update_cumulative_interest(position, opening_indexed_position); return Ok(true); } else if new_native_position < I80F48::ONE && allow_dusting { // if there's less than one token deposited, zero the position cm!(self.dust += new_native_position); cm!(self.indexed_borrows += position.indexed_position); position.indexed_position = I80F48::ZERO; + self.update_cumulative_interest(position, opening_indexed_position); return Ok(false); } @@ -256,6 +259,7 @@ impl Bank { let indexed_change = div_rounding_up(native_amount, self.deposit_index); cm!(self.indexed_deposits += indexed_change); cm!(position.indexed_position += indexed_change); + self.update_cumulative_interest(position, opening_indexed_position); Ok(true) } @@ -317,6 +321,7 @@ impl Bank { ) -> Result<(bool, I80F48)> { require_gte!(native_amount, 0); let native_position = position.native(self); + let opening_indexed_position = position.indexed_position; if native_position.is_positive() { let new_native_position = cm!(native_position - native_amount); @@ -327,12 +332,14 @@ impl Bank { cm!(self.dust += new_native_position); cm!(self.indexed_deposits -= position.indexed_position); position.indexed_position = I80F48::ZERO; + self.update_cumulative_interest(position, opening_indexed_position); return Ok((false, I80F48::ZERO)); } else { // withdraw some deposits leaving a positive balance let indexed_change = cm!(native_amount / self.deposit_index); cm!(self.indexed_deposits -= indexed_change); cm!(position.indexed_position -= indexed_change); + self.update_cumulative_interest(position, opening_indexed_position); return Ok((true, I80F48::ZERO)); } } @@ -355,6 +362,7 @@ impl Bank { let indexed_change = cm!(native_amount / self.borrow_index); cm!(self.indexed_borrows += indexed_change); cm!(position.indexed_position -= indexed_change); + self.update_cumulative_interest(position, opening_indexed_position); Ok((true, loan_origination_fee)) } @@ -401,6 +409,30 @@ impl Bank { } } + pub fn update_cumulative_interest( + &self, + position: &mut TokenPosition, + opening_indexed_position: I80F48, + ) { + if opening_indexed_position.is_positive() { + let interest = + cm!((self.deposit_index - position.previous_index) * opening_indexed_position) + .to_num::(); + position.cumulative_deposit_interest += interest; + } else { + let interest = + cm!((self.borrow_index - position.previous_index) * opening_indexed_position) + .to_num::(); + position.cumulative_borrow_interest += interest; + } + + if position.indexed_position.is_positive() { + position.previous_index = self.deposit_index + } else { + position.previous_index = self.borrow_index + } + } + pub fn compute_index( &self, indexed_total_deposits: I80F48, @@ -634,8 +666,11 @@ mod tests { indexed_position: I80F48::ZERO, token_index: 0, in_use_count: if is_in_use { 1 } else { 0 }, + cumulative_deposit_interest: 0.0, + cumulative_borrow_interest: 0.0, + previous_index: I80F48::ZERO, padding: Default::default(), - reserved: [0; 40], + reserved: [0; 16], }; account.indexed_position = indexed(I80F48::from_num(start), &bank); diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index a7a9a24ee..f16bacf2a 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -24,6 +24,7 @@ use super::TokenIndex; use super::FREE_ORDER_SLOT; use super::{HealthCache, HealthType}; use super::{PerpPosition, Serum3Orders, TokenPosition}; +use crate::logs::DeactivateTokenPositionLog; use checked_math as cm; type BorshVecLength = u32; @@ -72,10 +73,12 @@ pub struct MangoAccount { pub padding: [u8; 1], + // (Display only) // Cumulative (deposits - withdraws) // using USD prices at the time of the deposit/withdraw // in USD units with 6 decimals pub net_deposits: i64, + // (Display only) // Cumulative settles on perp positions // TODO: unimplemented pub net_settled: i64, @@ -616,8 +619,11 @@ impl< indexed_position: I80F48::ZERO, token_index, in_use_count: 0, + cumulative_deposit_interest: 0.0, + cumulative_borrow_interest: 0.0, + previous_index: I80F48::ZERO, padding: Default::default(), - reserved: [0; 40], + reserved: [0; 16], }; } Ok((v, raw_index, bank_index)) @@ -632,6 +638,24 @@ impl< self.token_position_mut_by_raw_index(raw_index).token_index = TokenIndex::MAX; } + pub fn deactivate_token_position_and_log( + &mut self, + raw_index: usize, + mango_account_pubkey: Pubkey, + ) { + let mango_group = self.fixed.deref_or_borrow().group; + let token_position = self.token_position_mut_by_raw_index(raw_index); + assert!(token_position.in_use_count == 0); + emit!(DeactivateTokenPositionLog { + mango_group: mango_group, + mango_account: mango_account_pubkey, + token_index: token_position.token_index, + cumulative_deposit_interest: token_position.cumulative_deposit_interest, + cumulative_borrow_interest: token_position.cumulative_borrow_interest, + }); + self.token_position_mut_by_raw_index(raw_index).token_index = TokenIndex::MAX; + } + // get mut Serum3Orders at raw_index pub fn serum3_orders_mut_by_raw_index(&mut self, raw_index: usize) -> &mut Serum3Orders { let offset = self.header().serum3_offset(raw_index); diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 32c4924d1..f774b1099 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -32,13 +32,24 @@ pub struct TokenPosition { pub padding: [u8; 5], #[derivative(Debug = "ignore")] - pub reserved: [u8; 40], + pub reserved: [u8; 16], + + // bookkeeping variable for onchain interest calculation + // either deposit_index or borrow_index at last indexed_position change + pub previous_index: I80F48, + + // (Display only) + // Cumulative deposit interest in token native units + pub cumulative_deposit_interest: f32, + // (Display only) + // Cumulative borrow interest in token native units + pub cumulative_borrow_interest: f32, } unsafe impl bytemuck::Pod for TokenPosition {} unsafe impl bytemuck::Zeroable for TokenPosition {} -const_assert_eq!(size_of::(), 24 + 40); +const_assert_eq!(size_of::(), 64); const_assert_eq!(size_of::() % 8, 0); impl Default for TokenPosition { @@ -47,8 +58,11 @@ impl Default for TokenPosition { indexed_position: I80F48::ZERO, token_index: TokenIndex::MAX, in_use_count: 0, + cumulative_deposit_interest: 0.0, + cumulative_borrow_interest: 0.0, + previous_index: I80F48::ZERO, padding: Default::default(), - reserved: [0; 40], + reserved: [0; 16], } } } diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index e5d24c10a..c65f1b7b6 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -4487,9 +4487,23 @@ export type MangoV4 = { "type": { "array": [ "u8", - 40 + 8 ] } + }, + { + "name": "previousIndex", + "type": { + "defined": "I80F48" + } + }, + { + "name": "cumulativeDepositInterest", + "type": "i64" + }, + { + "name": "cumulativeBorrowInterest", + "type": "i64" } ] } @@ -5978,6 +5992,36 @@ export type MangoV4 = { "index": false } ] + }, + { + "name": "DeactivateTokenPositionLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "mangoAccount", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "cumulativeDepositInterest", + "type": "i64", + "index": false + }, + { + "name": "cumulativeBorrowInterest", + "type": "i64", + "index": false + } + ] } ], "errors": [ @@ -10588,9 +10632,23 @@ export const IDL: MangoV4 = { "type": { "array": [ "u8", - 40 + 8 ] } + }, + { + "name": "previousIndex", + "type": { + "defined": "I80F48" + } + }, + { + "name": "cumulativeDepositInterest", + "type": "i64" + }, + { + "name": "cumulativeBorrowInterest", + "type": "i64" } ] } @@ -12079,6 +12137,36 @@ export const IDL: MangoV4 = { "index": false } ] + }, + { + "name": "DeactivateTokenPositionLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "mangoAccount", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "cumulativeDepositInterest", + "type": "i64", + "index": false + }, + { + "name": "cumulativeBorrowInterest", + "type": "i64", + "index": false + } + ] } ], "errors": [ From 8ba52f46c2f09d0782297b05280d2b59f85329a3 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 29 Sep 2022 12:59:55 +0200 Subject: [PATCH 25/39] Perp settle pnl: incentive for settler (#252) This adds three new options to perp markets: - a flat fee, that is paid for all perp settlements - given that they exceed the configured min settle amount threshold to be eligible for fees - and a fractional fee, that only applies when the benefiting account has low health and gradually becomes bigger as the account slides towards liquidation --- anchor-tests/test.ts | 7 +- client/src/client.rs | 62 +- client/src/context.rs | 57 ++ liquidator/src/liquidate.rs | 4 +- .../src/instructions/perp_create_market.rs | 9 +- .../src/instructions/perp_edit_market.rs | 13 + .../src/instructions/perp_settle_pnl.rs | 116 ++-- programs/mango-v4/src/lib.rs | 18 +- programs/mango-v4/src/state/perp_market.rs | 36 +- .../tests/program_test/mango_client.rs | 31 +- programs/mango-v4/tests/test_liq_perps.rs | 6 +- programs/mango-v4/tests/test_perp.rs | 6 +- programs/mango-v4/tests/test_perp_settle.rs | 572 ++++++++++++------ ts/client/src/client.ts | 12 + ts/client/src/mango_v4.ts | 142 ++++- ts/client/src/scripts/devnet-admin.ts | 8 + .../src/scripts/mb-liqtest-create-group.ts | 4 + 17 files changed, 767 insertions(+), 336 deletions(-) diff --git a/anchor-tests/test.ts b/anchor-tests/test.ts index 96a8984e3..eaf6c63ec 100644 --- a/anchor-tests/test.ts +++ b/anchor-tests/test.ts @@ -184,7 +184,6 @@ describe('mango-v4', () => { 0, 'BTC-PERP', 0.1, - 1, 6, 1, 10, @@ -196,9 +195,15 @@ describe('mango-v4', () => { 0.012, 0.0002, 0.0, + 0, 0.05, 0.05, 100, + true, + true, + 0, + 0, + 0, ); await group.reloadAll(envClient); }); diff --git a/client/src/client.rs b/client/src/client.rs index cff5ae093..cba8d94db 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -14,6 +14,7 @@ use anchor_spl::token::Token; use bincode::Options; use fixed::types::I80F48; use itertools::Itertools; + use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; use mango_v4::state::{ Bank, Group, MangoAccountValue, PerpMarketIndex, Serum3MarketIndex, TokenIndex, @@ -317,49 +318,13 @@ impl MangoClient { liqee: &MangoAccountValue, writable_banks: &[TokenIndex], ) -> anyhow::Result> { - // figure out all the banks/oracles that need to be passed for the health check - let mut banks = vec![]; - let mut oracles = vec![]; let account = self.mango_account()?; - - let token_indexes = liqee - .active_token_positions() - .chain(account.active_token_positions()) - .map(|ta| ta.token_index) - .unique(); - - for token_index in token_indexes { - let mint_info = self.context.mint_info(token_index); - let writable_bank = writable_banks.iter().contains(&token_index); - banks.push((mint_info.first_bank(), writable_bank)); - oracles.push(mint_info.oracle); - } - - let serum_oos = liqee - .active_serum3_orders() - .chain(account.active_serum3_orders()) - .map(|&s| s.open_orders); - let perp_markets = liqee - .active_perp_positions() - .chain(account.active_perp_positions()) - .map(|&pa| self.context.perp_market_address(pa.market_index)); - let perp_oracles = liqee - .active_perp_positions() - .chain(account.active_perp_positions()) - .map(|&pa| self.context.perp(pa.market_index).market.oracle); - - Ok(banks - .iter() - .map(|(pubkey, is_writable)| AccountMeta { - pubkey: *pubkey, - is_writable: *is_writable, - is_signer: false, - }) - .chain(oracles.into_iter().map(to_readonly_account_meta)) - .chain(perp_markets.map(to_readonly_account_meta)) - .chain(perp_oracles.map(to_readonly_account_meta)) - .chain(serum_oos.map(to_readonly_account_meta)) - .collect()) + self.context + .derive_health_check_remaining_account_metas_two_accounts( + &account, + liqee, + writable_banks, + ) } pub fn token_deposit(&self, mint: Pubkey, amount: u64) -> anyhow::Result { @@ -841,7 +806,7 @@ impl MangoClient { pub fn perp_settle_pnl( &self, market_index: PerpMarketIndex, - account_a: &Pubkey, + account_a: (&Pubkey, &MangoAccountValue), account_b: (&Pubkey, &MangoAccountValue), ) -> anyhow::Result { let perp = self.context.perp(market_index); @@ -849,7 +814,7 @@ impl MangoClient { let health_remaining_ams = self .context - .derive_health_check_remaining_account_metas(account_b.1, vec![], false) + .derive_health_check_remaining_account_metas_two_accounts(account_a.1, account_b.1, &[]) .unwrap(); self.program() @@ -860,8 +825,10 @@ impl MangoClient { 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, + account_a: *account_a.0, account_b: *account_b.0, oracle: perp.market.oracle, quote_bank: settlement_token.mint_info.first_bank(), @@ -871,10 +838,9 @@ impl MangoClient { ams.extend(health_remaining_ams.into_iter()); ams }, - data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpSettlePnl { - max_settle_amount: u64::MAX, - }), + data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpSettlePnl {}), }) + .signer(&self.owner) .send() .map_err(prettify_client_error) } diff --git a/client/src/context.rs b/client/src/context.rs index c83af29d5..bfa507210 100644 --- a/client/src/context.rs +++ b/client/src/context.rs @@ -10,6 +10,7 @@ use mango_v4::state::{ }; use fixed::types::I80F48; +use itertools::Itertools; use crate::gpa::*; @@ -264,6 +265,62 @@ impl MangoGroupContext { .chain(serum_oos.map(to_account_meta)) .collect()) } + + pub fn derive_health_check_remaining_account_metas_two_accounts( + &self, + account1: &MangoAccountValue, + account2: &MangoAccountValue, + writable_banks: &[TokenIndex], + ) -> anyhow::Result> { + // figure out all the banks/oracles that need to be passed for the health check + let mut banks = vec![]; + let mut oracles = vec![]; + + let token_indexes = account2 + .active_token_positions() + .chain(account1.active_token_positions()) + .map(|ta| ta.token_index) + .unique(); + + for token_index in token_indexes { + let mint_info = self.mint_info(token_index); + let writable_bank = writable_banks.iter().contains(&token_index); + banks.push((mint_info.first_bank(), writable_bank)); + oracles.push(mint_info.oracle); + } + + let serum_oos = account2 + .active_serum3_orders() + .chain(account1.active_serum3_orders()) + .map(|&s| s.open_orders); + let perp_markets = account2 + .active_perp_positions() + .chain(account1.active_perp_positions()) + .map(|&pa| self.perp_market_address(pa.market_index)); + let perp_oracles = account2 + .active_perp_positions() + .chain(account1.active_perp_positions()) + .map(|&pa| self.perp(pa.market_index).market.oracle); + + let to_account_meta = |pubkey| AccountMeta { + pubkey, + is_writable: false, + is_signer: false, + }; + + Ok(banks + .iter() + .map(|(pubkey, is_writable)| AccountMeta { + pubkey: *pubkey, + is_writable: *is_writable, + is_signer: false, + }) + .chain(oracles.into_iter().map(to_account_meta)) + .chain(perp_markets.map(to_account_meta)) + .chain(perp_oracles.map(to_account_meta)) + .chain(serum_oos.map(to_account_meta)) + .collect()) + } } fn from_serum_style_pubkey(d: [u64; 4]) -> Pubkey { diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 0ed792764..902e3a2cf 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -275,9 +275,9 @@ impl<'a> LiquidateHelper<'a> { let (counter_key, counter_acc, _) = counters.first().unwrap(); let (account_a, account_b) = if pnl > 0 { - (self.pubkey, (counter_key, counter_acc)) + ((self.pubkey, self.liqee), (counter_key, counter_acc)) } else { - (counter_key, (self.pubkey, self.liqee)) + ((counter_key, counter_acc), (self.pubkey, self.liqee)) }; let sig = self .client diff --git a/programs/mango-v4/src/instructions/perp_create_market.rs b/programs/mango-v4/src/instructions/perp_create_market.rs index 7b28dfd77..43fa71e74 100644 --- a/programs/mango-v4/src/instructions/perp_create_market.rs +++ b/programs/mango-v4/src/instructions/perp_create_market.rs @@ -65,6 +65,9 @@ pub fn perp_create_market( group_insurance_fund: bool, trusted_market: bool, fee_penalty: f32, + settle_fee_flat: f32, + settle_fee_amount_threshold: f32, + settle_fee_fraction_low_health: f32, ) -> Result<()> { let mut perp_market = ctx.accounts.perp_market.load_init()?; *perp_market = PerpMarket { @@ -94,7 +97,6 @@ pub fn perp_create_market( seq_num: 0, fees_accrued: I80F48::ZERO, fees_settled: I80F48::ZERO, - // Why optional - Perp could be based purely on an oracle bump: *ctx.bumps.get("perp_market").ok_or(MangoError::SomeError)?, base_decimals, perp_market_index, @@ -105,7 +107,10 @@ pub fn perp_create_market( padding1: Default::default(), padding2: Default::default(), fee_penalty, - reserved: [0; 108], + settle_fee_flat, + settle_fee_amount_threshold, + settle_fee_fraction_low_health, + reserved: [0; 92], }; let mut bids = ctx.accounts.bids.load_init()?; diff --git a/programs/mango-v4/src/instructions/perp_edit_market.rs b/programs/mango-v4/src/instructions/perp_edit_market.rs index 3508f603d..e5d353fdf 100644 --- a/programs/mango-v4/src/instructions/perp_edit_market.rs +++ b/programs/mango-v4/src/instructions/perp_edit_market.rs @@ -36,6 +36,9 @@ pub fn perp_edit_market( group_insurance_fund_opt: Option, trusted_market_opt: Option, fee_penalty_opt: Option, + settle_fee_flat_opt: Option, + settle_fee_amount_threshold_opt: Option, + settle_fee_fraction_low_health_opt: Option, ) -> Result<()> { let mut perp_market = ctx.accounts.perp_market.load_mut()?; @@ -122,5 +125,15 @@ pub fn perp_edit_market( perp_market.trusted_market = if trusted_market { 1 } else { 0 }; } + if let Some(settle_fee_flat) = settle_fee_flat_opt { + perp_market.settle_fee_flat = settle_fee_flat; + } + if let Some(settle_fee_amount_threshold) = settle_fee_amount_threshold_opt { + perp_market.settle_fee_amount_threshold = settle_fee_amount_threshold; + } + if let Some(settle_fee_fraction_low_health) = settle_fee_fraction_low_health_opt { + perp_market.settle_fee_fraction_low_health = settle_fee_fraction_low_health; + } + Ok(()) } diff --git a/programs/mango-v4/src/instructions/perp_settle_pnl.rs b/programs/mango-v4/src/instructions/perp_settle_pnl.rs index 510b507fd..ba6db6937 100644 --- a/programs/mango-v4/src/instructions/perp_settle_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_settle_pnl.rs @@ -4,12 +4,11 @@ use fixed::types::I80F48; use crate::accounts_zerocopy::*; use crate::error::*; -use crate::state::new_fixed_order_account_retriever; use crate::state::new_health_cache; use crate::state::Bank; use crate::state::HealthType; use crate::state::MangoAccount; -use crate::state::TokenPosition; +use crate::state::ScanningAccountRetriever; use crate::state::QUOTE_TOKEN_INDEX; use crate::state::{AccountLoaderDynamic, Group, PerpMarket}; @@ -17,6 +16,14 @@ use crate::state::{AccountLoaderDynamic, Group, PerpMarket}; pub struct PerpSettlePnl<'info> { pub group: AccountLoader<'info, Group>, + #[account( + mut, + has_one = group, + // settler_owner is checked at #1 + )] + pub settler: AccountLoaderDynamic<'info, MangoAccount>, + pub settler_owner: Signer<'info>, + #[account(has_one = group, has_one = oracle)] pub perp_market: AccountLoader<'info, PerpMarket>, @@ -34,20 +41,13 @@ pub struct PerpSettlePnl<'info> { pub quote_bank: AccountLoader<'info, Bank>, } -pub fn perp_settle_pnl(ctx: Context, max_settle_amount: u64) -> Result<()> { +pub fn perp_settle_pnl(ctx: Context) -> Result<()> { // Cannot settle with yourself require!( - ctx.accounts.account_a.to_account_info().key - != ctx.accounts.account_b.to_account_info().key, + ctx.accounts.account_a.key() != ctx.accounts.account_b.key(), MangoError::CannotSettleWithSelf ); - // max_settle_amount must greater than zero - require!( - max_settle_amount > 0, - MangoError::MaxSettleAmountMustBeGreaterThanZero - ); - let perp_market_index = { let perp_market = ctx.accounts.perp_market.load()?; perp_market.perp_market_index @@ -64,16 +64,25 @@ pub fn perp_settle_pnl(ctx: Context, max_settle_amount: u64) -> R account_b.token_position(QUOTE_TOKEN_INDEX)?; } + let a_init_health; + let a_maint_health; + let b_spot_health; + { + let retriever = + ScanningAccountRetriever::new(ctx.remaining_accounts, &ctx.accounts.group.key()) + .context("create account retriever")?; + b_spot_health = + new_health_cache(&account_b.borrow(), &retriever)?.spot_health(HealthType::Maint); + let a_cache = new_health_cache(&account_a.borrow(), &retriever)?; + a_init_health = a_cache.health(HealthType::Init); + a_maint_health = a_cache.health(HealthType::Maint); + }; + // Account B is the one that must have negative pnl. Check how much of that may be actualized // given the account's health. In that, we only care about the health of spot assets on the account. // Example: With +100 USDC and -2 SOL (-80 USD) and -500 USD PNL the account may still settle // 100 - 1.1*80 = 12 USD perp pnl, even though the overall health is already negative. - // Afterwards the account is perp-bankrupt. - let b_spot_health = { - let retriever = - new_fixed_order_account_retriever(ctx.remaining_accounts, &account_b.borrow())?; - new_health_cache(&account_b.borrow(), &retriever)?.spot_health(HealthType::Maint) - }; + // Further settlement would convert perp-losses into token-losses and isn't allowed. require!(b_spot_health >= 0, MangoError::HealthMustBePositive); let mut bank = ctx.accounts.quote_bank.load_mut()?; @@ -108,36 +117,67 @@ pub fn perp_settle_pnl(ctx: Context, max_settle_amount: u64) -> R require!(a_pnl.is_positive(), MangoError::ProfitabilityMismatch); require!(b_pnl.is_negative(), MangoError::ProfitabilityMismatch); - // Settle for the maximum possible capped to max_settle_amount and b's spot health - let settlement = a_pnl - .abs() - .min(b_pnl.abs()) - .min(b_spot_health) - .min(I80F48::from(max_settle_amount)); + // Settle for the maximum possible capped to b's spot health + let settlement = a_pnl.abs().min(b_pnl.abs()).min(b_spot_health); a_perp_position.change_quote_position(-settlement); b_perp_position.change_quote_position(settlement); - // Update the account's net_settled with the new PnL + // A percentage fee is paid to the settler when account_a's health is low. + // That's because the settlement could avoid it getting liquidated. + let low_health_fee = if a_init_health < 0 { + let fee_fraction = I80F48::from_num(perp_market.settle_fee_fraction_low_health); + if a_maint_health < 0 { + cm!(settlement * fee_fraction) + } else { + cm!(settlement * fee_fraction * (-a_init_health / (a_maint_health - a_init_health))) + } + } else { + I80F48::ZERO + }; + + // The settler receives a flat fee + let flat_fee = I80F48::from_num(perp_market.settle_fee_flat); + + // Fees only apply when the settlement is large enough + let fee = if settlement >= perp_market.settle_fee_amount_threshold { + cm!(low_health_fee + flat_fee).min(settlement) + } else { + I80F48::ZERO + }; + + // Update the account's net_settled with the new PnL. + // Applying the fee here means that it decreases the displayed perp pnl. let settlement_i64 = settlement.checked_to_num::().unwrap(); - cm!(account_a.fixed.net_settled += settlement_i64); + let fee_i64 = fee.checked_to_num::().unwrap(); + cm!(account_a.fixed.net_settled += settlement_i64 - fee_i64); cm!(account_b.fixed.net_settled -= settlement_i64); // Transfer token balances + // The fee is paid by the account with positive unsettled pnl let a_token_position = account_a.token_position_mut(QUOTE_TOKEN_INDEX)?.0; let b_token_position = account_b.token_position_mut(QUOTE_TOKEN_INDEX)?.0; - transfer_token_internal(&mut bank, b_token_position, a_token_position, settlement)?; + bank.deposit(a_token_position, cm!(settlement - fee))?; + bank.withdraw_with_fee(b_token_position, settlement)?; - msg!("settled pnl = {}", settlement); - Ok(()) -} - -fn transfer_token_internal( - bank: &mut Bank, - from_position: &mut TokenPosition, - to_position: &mut TokenPosition, - native_amount: I80F48, -) -> Result<()> { - bank.deposit(to_position, native_amount)?; - bank.withdraw_with_fee(from_position, native_amount)?; + // settler might be the same as account a or b + drop(account_a); + drop(account_b); + + let mut settler = ctx.accounts.settler.load_mut()?; + // account constraint #1 + require!( + settler + .fixed + .is_owner_or_delegate(ctx.accounts.settler_owner.key()), + MangoError::SomeError + ); + + let (settler_token_position, settler_token_raw_index, _) = + settler.ensure_token_position(QUOTE_TOKEN_INDEX)?; + if !bank.deposit(settler_token_position, fee)? { + settler.deactivate_token_position(settler_token_raw_index); + } + + msg!("settled pnl = {}, fee = {}", settlement, fee); Ok(()) } diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index d694a0b1e..5dfe4fd97 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -394,6 +394,9 @@ pub mod mango_v4 { group_insurance_fund: bool, trusted_market: bool, fee_penalty: f32, + settle_fee_flat: f32, + settle_fee_amount_threshold: f32, + settle_fee_fraction_low_health: f32, ) -> Result<()> { instructions::perp_create_market( ctx, @@ -410,12 +413,15 @@ pub mod mango_v4 { liquidation_fee, maker_fee, taker_fee, - max_funding, min_funding, + max_funding, impact_quantity, group_insurance_fund, trusted_market, fee_penalty, + settle_fee_flat, + settle_fee_amount_threshold, + settle_fee_fraction_low_health, ) } @@ -438,6 +444,9 @@ pub mod mango_v4 { group_insurance_fund_opt: Option, trusted_market_opt: Option, fee_penalty_opt: Option, + settle_fee_flat_opt: Option, + settle_fee_amount_threshold_opt: Option, + settle_fee_fraction_low_health_opt: Option, ) -> Result<()> { instructions::perp_edit_market( ctx, @@ -457,6 +466,9 @@ pub mod mango_v4 { group_insurance_fund_opt, trusted_market_opt, fee_penalty_opt, + settle_fee_flat_opt, + settle_fee_amount_threshold_opt, + settle_fee_fraction_low_health_opt, ) } @@ -526,8 +538,8 @@ pub mod mango_v4 { instructions::perp_update_funding(ctx) } - pub fn perp_settle_pnl(ctx: Context, max_settle_amount: u64) -> Result<()> { - instructions::perp_settle_pnl(ctx, max_settle_amount) + pub fn perp_settle_pnl(ctx: Context) -> Result<()> { + instructions::perp_settle_pnl(ctx) } pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> Result<()> { diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 91a621181..d3cb77f46 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -101,29 +101,18 @@ pub struct PerpMarket { pub fee_penalty: f32, - pub reserved: [u8; 108], + /// In native units of settlement token, given to each settle call above the + /// settle_fee_amount_threshold. + pub settle_fee_flat: f32, + /// Pnl settlement amount needed to be eligible for fees. + pub settle_fee_amount_threshold: f32, + /// Fraction of pnl to pay out as fee if +pnl account has low health. + pub settle_fee_fraction_low_health: f32, + + pub reserved: [u8; 92], } -const_assert_eq!( - size_of::(), - 32 + 2 - + 2 - + 4 - + 16 - + 32 - + 16 - + 32 * 3 - + 8 * 2 - + 16 * 12 - + 8 * 2 - + 8 * 2 - + 16 - + 2 - + 6 - + 8 - + 4 - + 108 -); +const_assert_eq!(size_of::(), 584); const_assert_eq!(size_of::() % 8, 0); impl PerpMarket { @@ -278,7 +267,7 @@ impl PerpMarket { fees_settled: I80F48::ZERO, bump: 0, base_decimals: 0, - reserved: [0; 108], + reserved: [0; 92], padding0: Default::default(), padding1: Default::default(), padding2: Default::default(), @@ -286,6 +275,9 @@ impl PerpMarket { fee_penalty: 0.0, trusted_market: 0, group_insurance_fund: 0, + settle_fee_flat: 0.0, + settle_fee_amount_threshold: 0.0, + settle_fee_fraction_low_health: 0.0, } } } diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 02c148597..ccb5fcb56 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -2144,6 +2144,9 @@ pub struct PerpCreateMarketInstruction { pub group_insurance_fund: bool, pub trusted_market: bool, pub fee_penalty: f32, + pub settle_fee_flat: f32, + pub settle_fee_amount_threshold: f32, + pub settle_fee_fraction_low_health: f32, } impl PerpCreateMarketInstruction { pub async fn with_new_book_and_queue( @@ -2197,6 +2200,9 @@ impl ClientInstruction for PerpCreateMarketInstruction { group_insurance_fund: self.group_insurance_fund, trusted_market: self.trusted_market, fee_penalty: self.fee_penalty, + settle_fee_flat: self.settle_fee_flat, + settle_fee_amount_threshold: self.settle_fee_amount_threshold, + settle_fee_fraction_low_health: self.settle_fee_fraction_low_health, }; let perp_market = Pubkey::find_program_address( @@ -2554,11 +2560,12 @@ impl ClientInstruction for PerpUpdateFundingInstruction { } pub struct PerpSettlePnlInstruction { + pub settler: Pubkey, + pub settler_owner: TestKeypair, pub account_a: Pubkey, pub account_b: Pubkey, pub perp_market: Pubkey, pub quote_bank: Pubkey, - pub max_settle_amount: u64, } #[async_trait::async_trait(?Send)] impl ClientInstruction for PerpSettlePnlInstruction { @@ -2569,26 +2576,32 @@ impl ClientInstruction for PerpSettlePnlInstruction { account_loader: impl ClientAccountLoader + 'async_trait, ) -> (Self::Accounts, instruction::Instruction) { let program_id = mango_v4::id(); - let instruction = Self::Instruction { - max_settle_amount: self.max_settle_amount, - }; + let instruction = Self::Instruction {}; let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap(); + let account_a = account_loader + .load_mango_account(&self.account_a) + .await + .unwrap(); let account_b = account_loader .load_mango_account(&self.account_b) .await .unwrap(); - let health_check_metas = derive_health_check_remaining_account_metas( + let health_check_metas = derive_liquidation_remaining_account_metas( &account_loader, + &account_a, &account_b, - None, - false, - Some(perp_market.perp_market_index), + TokenIndex::MAX, + 0, + TokenIndex::MAX, + 0, ) .await; let accounts = Self::Accounts { group: perp_market.group, + settler: self.settler, + settler_owner: self.settler_owner.pubkey(), perp_market: self.perp_market, account_a: self.account_a, account_b: self.account_b, @@ -2603,7 +2616,7 @@ impl ClientInstruction for PerpSettlePnlInstruction { } fn signers(&self) -> Vec { - vec![] + vec![self.settler_owner] } } diff --git a/programs/mango-v4/tests/test_liq_perps.rs b/programs/mango-v4/tests/test_liq_perps.rs index c72655a06..27738595b 100644 --- a/programs/mango-v4/tests/test_liq_perps.rs +++ b/programs/mango-v4/tests/test_liq_perps.rs @@ -249,6 +249,9 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr 0, ) .await; + let settler = + create_funded_account(&solana, group, owner, 251, &context.users[1], &[], 0, 0).await; + let settler_owner = owner.clone(); // // TEST: Create a perp market @@ -512,11 +515,12 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr send_tx( solana, PerpSettlePnlInstruction { + settler, + settler_owner, account_a: liqor, account_b: account_1, perp_market, quote_bank: tokens[0].bank, - max_settle_amount: u64::MAX, }, ) .await diff --git a/programs/mango-v4/tests/test_perp.rs b/programs/mango-v4/tests/test_perp.rs index e0a60362b..975f6afea 100644 --- a/programs/mango-v4/tests/test_perp.rs +++ b/programs/mango-v4/tests/test_perp.rs @@ -58,6 +58,9 @@ async fn test_perp() -> Result<(), TransportError> { 0, ) .await; + let settler = + create_funded_account(&solana, group, owner, 251, &context.users[1], &[], 0, 0).await; + let settler_owner = owner.clone(); // // TEST: Create a perp market @@ -384,11 +387,12 @@ async fn test_perp() -> Result<(), TransportError> { send_tx( solana, PerpSettlePnlInstruction { + settler, + settler_owner, account_a: account_0, account_b: account_1, perp_market, quote_bank: tokens[0].bank, - max_settle_amount: u64::MAX, }, ) .await diff --git a/programs/mango-v4/tests/test_perp_settle.rs b/programs/mango-v4/tests/test_perp_settle.rs index a54f0be20..079c50a6e 100644 --- a/programs/mango-v4/tests/test_perp_settle.rs +++ b/programs/mango-v4/tests/test_perp_settle.rs @@ -18,11 +18,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { let owner = context.users[0].key; let payer = context.users[1].key; let mints = &context.mints[0..=2]; - let payer_mint_accounts = &context.users[1].token_accounts[0..=2]; - let initial_token_deposit0 = 10_000; - // only deposited because perps currently require the base token position to be active - let initial_token_deposit1 = 1; + let initial_token_deposit = 10_000; // // SETUP: Create a group and an account @@ -37,98 +34,32 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { .create(solana) .await; - let account_0 = send_tx( - solana, - AccountCreateInstruction { - account_num: 0, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, + let settler = + create_funded_account(&solana, group, owner, 251, &context.users[1], &[], 0, 0).await; + let settler_owner = owner.clone(); + + let account_0 = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + &mints[0..1], + initial_token_deposit, + 0, ) - .await - .unwrap() - .account; - - let account_1 = send_tx( - solana, - AccountCreateInstruction { - account_num: 1, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, + .await; + let account_1 = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + &mints[0..1], + initial_token_deposit, + 0, ) - .await - .unwrap() - .account; - - // - // SETUP: Deposit user funds - // - { - send_tx( - solana, - TokenDepositInstruction { - amount: initial_token_deposit0, - account: account_0, - token_account: payer_mint_accounts[0], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - - send_tx( - solana, - TokenDepositInstruction { - amount: initial_token_deposit1, - account: account_0, - token_account: payer_mint_accounts[1], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - } - - { - send_tx( - solana, - TokenDepositInstruction { - amount: initial_token_deposit0, - account: account_1, - token_account: payer_mint_accounts[0], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - - send_tx( - solana, - TokenDepositInstruction { - amount: initial_token_deposit1, - account: account_1, - token_account: payer_mint_accounts[1], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - } + .await; // // TEST: Create a perp market @@ -264,11 +195,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { let result = send_tx( solana, PerpSettlePnlInstruction { + settler, + settler_owner, account_a: account_1, account_b: account_0, perp_market, quote_bank: tokens[1].bank, - max_settle_amount: u64::MAX, }, ) .await; @@ -283,11 +215,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { let result = send_tx( solana, PerpSettlePnlInstruction { + settler, + settler_owner, account_a: account_0, account_b: account_0, perp_market, quote_bank: tokens[0].bank, - max_settle_amount: u64::MAX, }, ) .await; @@ -302,11 +235,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { let result = send_tx( solana, PerpSettlePnlInstruction { + settler, + settler_owner, account_a: account_0, account_b: account_1, perp_market: perp_market_2, quote_bank: tokens[0].bank, - max_settle_amount: u64::MAX, }, ) .await; @@ -317,25 +251,6 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { "Cannot settle a position that does not exist".to_string(), ); - // max_settle_amount must be greater than zero - let result = send_tx( - solana, - PerpSettlePnlInstruction { - account_a: account_0, - account_b: account_1, - perp_market: perp_market, - quote_bank: tokens[0].bank, - max_settle_amount: 0, - }, - ) - .await; - - assert_mango_error( - &result, - MangoError::MaxSettleAmountMustBeGreaterThanZero.into(), - "max_settle_amount must be greater than zero".to_string(), - ); - // TODO: Test funding settlement { @@ -344,12 +259,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { let bank = solana.get_account::(tokens[0].bank).await; assert_eq!( mango_account_0.tokens[0].native(&bank).round(), - initial_token_deposit0, + initial_token_deposit, "account 0 has expected amount of tokens" ); assert_eq!( mango_account_1.tokens[0].native(&bank).round(), - initial_token_deposit0, + initial_token_deposit, "account 1 has expected amount of tokens" ); } @@ -372,11 +287,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { let result = send_tx( solana, PerpSettlePnlInstruction { + settler, + settler_owner, account_a: account_1, account_b: account_0, perp_market, quote_bank: tokens[0].bank, - max_settle_amount: u64::MAX, }, ) .await; @@ -418,70 +334,6 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { ); } - // Partially execute the settle - let partial_settle_amount = 200; - send_tx( - solana, - PerpSettlePnlInstruction { - account_a: account_0, - account_b: account_1, - perp_market, - quote_bank: tokens[0].bank, - max_settle_amount: partial_settle_amount, - }, - ) - .await - .unwrap(); - - { - let bank = solana.get_account::(tokens[0].bank).await; - let mango_account_0 = solana.get_account::(account_0).await; - let mango_account_1 = solana.get_account::(account_1).await; - - assert_eq!( - mango_account_0.perps[0].base_position_lots(), - 1, - "base position unchanged for account 0" - ); - assert_eq!( - mango_account_1.perps[0].base_position_lots(), - -1, - "base position unchanged for account 1" - ); - - assert_eq!( - mango_account_0.perps[0].quote_position_native().round(), - I80F48::from(-100_020) - I80F48::from(partial_settle_amount), - "quote position reduced for profitable position by max_settle_amount" - ); - assert_eq!( - mango_account_1.perps[0].quote_position_native().round(), - I80F48::from(100_000 + partial_settle_amount), - "quote position increased for losing position by opposite of first account" - ); - - assert_eq!( - mango_account_0.tokens[0].native(&bank).round(), - I80F48::from(initial_token_deposit0 + partial_settle_amount), - "account 0 token native position increased (profit) by max_settle_amount" - ); - assert_eq!( - mango_account_1.tokens[0].native(&bank).round(), - I80F48::from(initial_token_deposit0) - I80F48::from(partial_settle_amount), - "account 1 token native position decreased (loss) by max_settle_amount" - ); - - assert_eq!( - mango_account_0.net_settled, partial_settle_amount as i64, - "net_settled on account 0 updated with profit from settlement" - ); - assert_eq!( - mango_account_1.net_settled, - -(partial_settle_amount as i64), - "net_settled on account 1 updated with loss from settlement" - ); - } - // Change the oracle to a very high price, such that the pnl exceeds the account funding send_tx( solana, @@ -496,8 +348,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { .await .unwrap(); - let expected_pnl_0 = I80F48::from(50000 - 20 - partial_settle_amount as i64); - let expected_pnl_1 = I80F48::from(-50000 + partial_settle_amount as i64); + let expected_pnl_0 = I80F48::from(50000 - 20); + let expected_pnl_1 = I80F48::from(-50000); { let mango_account_0 = solana.get_account::(account_0).await; @@ -514,16 +366,17 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { } // Settle as much PNL as account_1's health allows - let account_1_health_non_perp = I80F48::from(9040); // 0.8 * (10000-200+1*1500) - let expected_total_settle = I80F48::from(partial_settle_amount) + account_1_health_non_perp; + let account_1_health_non_perp = I80F48::from_num(0.8 * 10000.0); + let expected_total_settle = account_1_health_non_perp; send_tx( solana, PerpSettlePnlInstruction { + settler, + settler_owner, account_a: account_0, account_b: account_1, perp_market, quote_bank: tokens[0].bank, - max_settle_amount: u64::MAX, }, ) .await @@ -558,12 +411,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { assert_eq!( mango_account_0.tokens[0].native(&bank).round(), - I80F48::from(initial_token_deposit0) + expected_total_settle, + I80F48::from(initial_token_deposit) + expected_total_settle, "account 0 token native position increased (profit)" ); assert_eq!( mango_account_1.tokens[0].native(&bank).round(), - I80F48::from(initial_token_deposit0) - expected_total_settle, + I80F48::from(initial_token_deposit) - expected_total_settle, "account 1 token native position decreased (loss)" ); @@ -591,8 +444,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { .await .unwrap(); - let expected_pnl_0 = I80F48::from(-9760); - let expected_pnl_1 = I80F48::from(9740); + let expected_pnl_0 = I80F48::from(-8520); + let expected_pnl_1 = I80F48::from(8500); { let mango_account_0 = solana.get_account::(account_0).await; @@ -613,11 +466,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { send_tx( solana, PerpSettlePnlInstruction { + settler, + settler_owner, account_a: account_1, account_b: account_0, perp_market, quote_bank: tokens[0].bank, - max_settle_amount: u64::MAX, }, ) .await @@ -653,12 +507,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { // 480 was previous settlement assert_eq!( mango_account_0.tokens[0].native(&bank).round(), - I80F48::from(initial_token_deposit0) + expected_total_settle, + I80F48::from(initial_token_deposit) + expected_total_settle, "account 0 token native position decreased (loss)" ); assert_eq!( mango_account_1.tokens[0].native(&bank).round(), - I80F48::from(initial_token_deposit0) - expected_total_settle, + I80F48::from(initial_token_deposit) - expected_total_settle, "account 1 token native position increased (profit)" ); @@ -689,3 +543,331 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { Ok(()) } + +#[tokio::test] +async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..=2]; + + let initial_token_deposit = 10_000; + + // + // SETUP: Create a group and accounts + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + zero_token_is_quote: true, + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + let quote_bank = tokens[0].bank; + + // ensure vaults are not empty + create_funded_account( + &solana, + group, + owner, + 250, + &context.users[1], + mints, + 100_000, + 0, + ) + .await; + + let settler = + create_funded_account(&solana, group, owner, 251, &context.users[1], &[], 0, 0).await; + let settler_owner = owner.clone(); + + let account_0 = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + &mints[0..1], + initial_token_deposit, + 0, + ) + .await; + let account_1 = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + &mints[0..1], + initial_token_deposit, + 0, + ) + .await; + + // + // SETUP: Create a perp market + // + let flat_fee = 1000; + let fee_low_health = 0.05; + let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx( + solana, + PerpCreateMarketInstruction { + group, + admin, + payer, + perp_market_index: 0, + quote_lot_size: 10, + base_lot_size: 100, + maint_asset_weight: 1.0, + init_asset_weight: 1.0, + maint_liab_weight: 1.0, + init_liab_weight: 1.0, + liquidation_fee: 0.0, + maker_fee: 0.0, + taker_fee: 0.0, + settle_fee_flat: flat_fee as f32, + settle_fee_amount_threshold: 2000.0, + settle_fee_fraction_low_health: fee_low_health, + ..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await + }, + ) + .await + .unwrap(); + + let price_lots = { + let perp_market = solana.get_account::(perp_market).await; + perp_market.native_price_to_lot(I80F48::from(1000)) + }; + + // Set the initial oracle price + send_tx( + solana, + StubOracleSetInstruction { + group, + admin, + mint: mints[1].pubkey, + payer, + price: "1000.0", + }, + ) + .await + .unwrap(); + + // + // SETUP: Create a perp base position + // + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Bid, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + client_order_id: 0, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + client_order_id: 0, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + PerpConsumeEventsInstruction { + perp_market, + mango_accounts: vec![account_0, account_1], + }, + ) + .await + .unwrap(); + + { + let mango_account_0 = solana.get_account::(account_0).await; + let mango_account_1 = solana.get_account::(account_1).await; + + assert_eq!(mango_account_0.perps[0].base_position_lots(), 1); + assert_eq!(mango_account_1.perps[0].base_position_lots(), -1); + assert_eq!( + mango_account_0.perps[0].quote_position_native().round(), + -100_000 + ); + assert_eq!(mango_account_1.perps[0].quote_position_native(), 100_000); + } + + // + // TEST: Settle (health is high) + // + send_tx( + solana, + StubOracleSetInstruction { + group, + admin, + mint: mints[1].pubkey, + payer, + price: "1050.0", + }, + ) + .await + .unwrap(); + + let expected_pnl = 5000; + + send_tx( + solana, + PerpSettlePnlInstruction { + settler, + settler_owner, + account_a: account_0, + account_b: account_1, + perp_market, + quote_bank, + }, + ) + .await + .unwrap(); + + let mut total_settled_pnl = expected_pnl; + let mut total_fees_paid = flat_fee; + { + let mango_account_0 = solana.get_account::(account_0).await; + let mango_account_1 = solana.get_account::(account_1).await; + assert_eq!( + mango_account_0.perps[0].quote_position_native().round(), + I80F48::from(-100_000 - total_settled_pnl) + ); + assert_eq!( + mango_account_1.perps[0].quote_position_native().round(), + I80F48::from(100_000 + total_settled_pnl), + ); + assert_eq!( + account_position(solana, account_0, quote_bank).await, + initial_token_deposit as i64 + total_settled_pnl - total_fees_paid + ); + assert_eq!( + account_position(solana, account_1, quote_bank).await, + initial_token_deposit as i64 - total_settled_pnl + ); + assert_eq!( + account_position(solana, settler, quote_bank).await, + total_fees_paid + ); + } + + // + // Bring account_0 health low, specifically to + // init_health = 14000 - 1.4 * 1 * 10700 = -980 + // maint_health = 14000 - 1.2 * 1 * 10700 = 1160 + // + send_tx( + solana, + TokenWithdrawInstruction { + account: account_0, + owner, + token_account: context.users[1].token_accounts[2], + amount: 1, + allow_borrow: true, + bank_index: 0, + }, + ) + .await + .unwrap(); + send_tx( + solana, + StubOracleSetInstruction { + group, + admin, + mint: mints[2].pubkey, + payer, + price: "10700.0", + }, + ) + .await + .unwrap(); + + // + // TEST: Settle (health is low) + // + send_tx( + solana, + StubOracleSetInstruction { + group, + admin, + mint: mints[1].pubkey, + payer, + price: "1100.0", + }, + ) + .await + .unwrap(); + + let expected_pnl = 5000; + + send_tx( + solana, + PerpSettlePnlInstruction { + settler, + settler_owner, + account_a: account_0, + account_b: account_1, + perp_market, + quote_bank, + }, + ) + .await + .unwrap(); + + total_settled_pnl += expected_pnl; + total_fees_paid += flat_fee + + (expected_pnl as f64 * fee_low_health as f64 * 980.0 / (1160.0 + 980.0)) as i64 + + 1; + { + let mango_account_0 = solana.get_account::(account_0).await; + let mango_account_1 = solana.get_account::(account_1).await; + assert_eq!( + mango_account_0.perps[0].quote_position_native().round(), + I80F48::from(-100_000 - total_settled_pnl) + ); + assert_eq!( + mango_account_1.perps[0].quote_position_native().round(), + I80F48::from(100_000 + total_settled_pnl), + ); + assert_eq!( + account_position(solana, account_0, quote_bank).await, + initial_token_deposit as i64 + total_settled_pnl - total_fees_paid + ); + assert_eq!( + account_position(solana, account_1, quote_bank).await, + initial_token_deposit as i64 - total_settled_pnl + ); + assert_eq!( + account_position(solana, settler, quote_bank).await, + total_fees_paid + ); + } + + Ok(()) +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 32763a374..0b4fb1fc2 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1404,6 +1404,9 @@ export class MangoClient { impactQuantity: number, groupInsuranceFund: boolean, trustedMarket: boolean, + settleFeeFlat: number, + settleFeeAmountThreshold: number, + settleFeeFractionLowHealth: number, ): Promise { const bids = new Keypair(); const asks = new Keypair(); @@ -1434,6 +1437,9 @@ export class MangoClient { groupInsuranceFund, trustedMarket, feePenalty, + settleFeeFlat, + settleFeeAmountThreshold, + settleFeeFractionLowHealth, ) .accounts({ group: group.publicKey, @@ -1506,6 +1512,9 @@ export class MangoClient { impactQuantity: number, groupInsuranceFund: boolean, trustedMarket: boolean, + settleFeeFlat: number, + settleFeeAmountThreshold: number, + settleFeeFractionLowHealth: number, ): Promise { const perpMarket = group.perpMarketsMap.get(perpMarketName)!; @@ -1531,6 +1540,9 @@ export class MangoClient { groupInsuranceFund, trustedMarket, feePenalty, + settleFeeFlat, + settleFeeAmountThreshold, + settleFeeFractionLowHealth, ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index c65f1b7b6..6bd4d1f54 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -2319,6 +2319,18 @@ export type MangoV4 = { { "name": "feePenalty", "type": "f32" + }, + { + "name": "settleFeeFlat", + "type": "f32" + }, + { + "name": "settleFeeAmountThreshold", + "type": "f32" + }, + { + "name": "settleFeeFractionLowHealth", + "type": "f32" } ] }, @@ -2439,6 +2451,24 @@ export type MangoV4 = { "type": { "option": "f32" } + }, + { + "name": "settleFeeFlatOpt", + "type": { + "option": "f32" + } + }, + { + "name": "settleFeeAmountThresholdOpt", + "type": { + "option": "f32" + } + }, + { + "name": "settleFeeFractionLowHealthOpt", + "type": { + "option": "f32" + } } ] }, @@ -2834,6 +2864,16 @@ export type MangoV4 = { "isMut": false, "isSigner": false }, + { + "name": "settler", + "isMut": true, + "isSigner": false + }, + { + "name": "settlerOwner", + "isMut": false, + "isSigner": true + }, { "name": "perpMarket", "isMut": false, @@ -2860,12 +2900,7 @@ export type MangoV4 = { "isSigner": false } ], - "args": [ - { - "name": "maxSettleAmount", - "type": "u64" - } - ] + "args": [] }, { "name": "perpSettleFees", @@ -4028,12 +4063,34 @@ export type MangoV4 = { "name": "feePenalty", "type": "f32" }, + { + "name": "settleFeeFlat", + "docs": [ + "In native units of settlement token, given to each settle call above the", + "settle_fee_amount_threshold." + ], + "type": "f32" + }, + { + "name": "settleFeeAmountThreshold", + "docs": [ + "Pnl settlement amount needed to be eligible for fees." + ], + "type": "f32" + }, + { + "name": "settleFeeFractionLowHealth", + "docs": [ + "Fraction of pnl to pay out as fee if +pnl account has low health." + ], + "type": "f32" + }, { "name": "reserved", "type": { "array": [ "u8", - 108 + 92 ] } } @@ -8464,6 +8521,18 @@ export const IDL: MangoV4 = { { "name": "feePenalty", "type": "f32" + }, + { + "name": "settleFeeFlat", + "type": "f32" + }, + { + "name": "settleFeeAmountThreshold", + "type": "f32" + }, + { + "name": "settleFeeFractionLowHealth", + "type": "f32" } ] }, @@ -8584,6 +8653,24 @@ export const IDL: MangoV4 = { "type": { "option": "f32" } + }, + { + "name": "settleFeeFlatOpt", + "type": { + "option": "f32" + } + }, + { + "name": "settleFeeAmountThresholdOpt", + "type": { + "option": "f32" + } + }, + { + "name": "settleFeeFractionLowHealthOpt", + "type": { + "option": "f32" + } } ] }, @@ -8979,6 +9066,16 @@ export const IDL: MangoV4 = { "isMut": false, "isSigner": false }, + { + "name": "settler", + "isMut": true, + "isSigner": false + }, + { + "name": "settlerOwner", + "isMut": false, + "isSigner": true + }, { "name": "perpMarket", "isMut": false, @@ -9005,12 +9102,7 @@ export const IDL: MangoV4 = { "isSigner": false } ], - "args": [ - { - "name": "maxSettleAmount", - "type": "u64" - } - ] + "args": [] }, { "name": "perpSettleFees", @@ -10173,12 +10265,34 @@ export const IDL: MangoV4 = { "name": "feePenalty", "type": "f32" }, + { + "name": "settleFeeFlat", + "docs": [ + "In native units of settlement token, given to each settle call above the", + "settle_fee_amount_threshold." + ], + "type": "f32" + }, + { + "name": "settleFeeAmountThreshold", + "docs": [ + "Pnl settlement amount needed to be eligible for fees." + ], + "type": "f32" + }, + { + "name": "settleFeeFractionLowHealth", + "docs": [ + "Fraction of pnl to pay out as fee if +pnl account has low health." + ], + "type": "f32" + }, { "name": "reserved", "type": { "array": [ "u8", - 108 + 92 ] } } diff --git a/ts/client/src/scripts/devnet-admin.ts b/ts/client/src/scripts/devnet-admin.ts index f15a98afb..5bf5dc558 100644 --- a/ts/client/src/scripts/devnet-admin.ts +++ b/ts/client/src/scripts/devnet-admin.ts @@ -374,11 +374,15 @@ async function main() { 0.012, 0.0002, 0.0, + 0, 0.05, 0.05, 100, true, true, + 1000, + 1000000, + 0.05, ); console.log('done'); } catch (error) { @@ -500,11 +504,15 @@ async function main() { 0.012, 0.0002, 0.0, + 0, 0.05, 0.05, 100, true, true, + 1000, + 1000000, + 0.05, ); console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); await group.reloadAll(client); diff --git a/ts/client/src/scripts/mb-liqtest-create-group.ts b/ts/client/src/scripts/mb-liqtest-create-group.ts index e464cd6eb..28c7a0f18 100644 --- a/ts/client/src/scripts/mb-liqtest-create-group.ts +++ b/ts/client/src/scripts/mb-liqtest-create-group.ts @@ -214,11 +214,15 @@ async function main() { 0.05, -0.001, 0.002, + 0, -0.1, 0.1, 10, false, false, + 0, + 0, + 0, ); } catch (error) { console.log(error); From 7e180c7b3a427fe3356311b47826ff52032247d1 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 29 Sep 2022 14:35:01 +0200 Subject: [PATCH 26/39] Perps: Support trusted markets --- client/src/perp_pnl.rs | 10 +++--- liquidator/src/liquidate.rs | 6 ++-- .../src/instructions/perp_settle_pnl.rs | 11 +++---- programs/mango-v4/src/state/health.rs | 32 ++++++++++++++++--- programs/mango-v4/src/state/perp_market.rs | 4 +++ programs/mango-v4/tests/test_liq_perps.rs | 4 +-- ts/client/src/mango_v4.ts | 28 ++++++++++------ 7 files changed, 64 insertions(+), 31 deletions(-) diff --git a/client/src/perp_pnl.rs b/client/src/perp_pnl.rs index 22b1b3feb..454abbf65 100644 --- a/client/src/perp_pnl.rs +++ b/client/src/perp_pnl.rs @@ -66,7 +66,7 @@ pub fn fetch_top( } } - // Negative pnl needs to be limited by spot_health. + // Negative pnl needs to be limited by perp_settle_health. // We're doing it in a second step, because it's pretty expensive and we don't // want to run this for all accounts. if direction == Direction::MaxNegative { @@ -78,10 +78,10 @@ pub fn fetch_top( } else { I80F48::ZERO }; - let spot_health = crate::health_cache::new(context, account_fetcher, &acc)? - .spot_health(HealthType::Maint); - let settleable_pnl = if spot_health > 0 && !acc.being_liquidated() { - (*pnl).max(-spot_health) + let perp_settle_health = + crate::health_cache::new(context, account_fetcher, &acc)?.perp_settle_health(); + let settleable_pnl = if perp_settle_health > 0 && !acc.being_liquidated() { + (*pnl).max(-perp_settle_health) } else { I80F48::ZERO }; diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 902e3a2cf..c5269ae31 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -226,7 +226,7 @@ impl<'a> LiquidateHelper<'a> { } fn perp_settle_pnl(&self) -> anyhow::Result> { - let spot_health = self.health_cache.spot_health(HealthType::Maint); + let perp_settle_health = self.health_cache.perp_settle_health(); let mut perp_settleable_pnl = self .liqee .active_perp_positions() @@ -237,8 +237,8 @@ impl<'a> LiquidateHelper<'a> { let pnl = pp.quote_position_native(); let settleable_pnl = if pnl > 0 { pnl - } else if pnl < 0 && spot_health > 0 { - pnl.max(-spot_health) + } else if pnl < 0 && perp_settle_health > 0 { + pnl.max(-perp_settle_health) } else { return None; }; diff --git a/programs/mango-v4/src/instructions/perp_settle_pnl.rs b/programs/mango-v4/src/instructions/perp_settle_pnl.rs index ba6db6937..f29df0103 100644 --- a/programs/mango-v4/src/instructions/perp_settle_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_settle_pnl.rs @@ -66,13 +66,12 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { let a_init_health; let a_maint_health; - let b_spot_health; + let b_settle_health; { let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &ctx.accounts.group.key()) .context("create account retriever")?; - b_spot_health = - new_health_cache(&account_b.borrow(), &retriever)?.spot_health(HealthType::Maint); + b_settle_health = new_health_cache(&account_b.borrow(), &retriever)?.perp_settle_health(); let a_cache = new_health_cache(&account_a.borrow(), &retriever)?; a_init_health = a_cache.health(HealthType::Init); a_maint_health = a_cache.health(HealthType::Maint); @@ -83,7 +82,7 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { // Example: With +100 USDC and -2 SOL (-80 USD) and -500 USD PNL the account may still settle // 100 - 1.1*80 = 12 USD perp pnl, even though the overall health is already negative. // Further settlement would convert perp-losses into token-losses and isn't allowed. - require!(b_spot_health >= 0, MangoError::HealthMustBePositive); + require!(b_settle_health >= 0, MangoError::HealthMustBePositive); let mut bank = ctx.accounts.quote_bank.load_mut()?; let perp_market = ctx.accounts.perp_market.load()?; @@ -117,8 +116,8 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { require!(a_pnl.is_positive(), MangoError::ProfitabilityMismatch); require!(b_pnl.is_negative(), MangoError::ProfitabilityMismatch); - // Settle for the maximum possible capped to b's spot health - let settlement = a_pnl.abs().min(b_pnl.abs()).min(b_spot_health); + // Settle for the maximum possible capped to b's settle health + let settlement = a_pnl.abs().min(b_pnl.abs()).min(b_settle_health); a_perp_position.change_quote_position(-settlement); b_perp_position.change_quote_position(settlement); diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 31150525c..19e9c52cf 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -526,6 +526,7 @@ pub struct PerpInfo { pub quote: I80F48, pub oracle_price: I80F48, pub has_open_orders: bool, + pub trusted_market: bool, } impl PerpInfo { @@ -612,13 +613,14 @@ impl PerpInfo { quote, oracle_price, has_open_orders: perp_position.has_open_orders(), + trusted_market: perp_market.trusted_market(), }) } /// Total health contribution from perp balances /// /// Due to isolation of perp markets, users may never borrow against perp - /// positions without settling first: perp health is capped at zero. + /// positions in untrusted without settling first: perp health is capped at zero. /// /// Users need to settle their perp pnl with other perp market participants /// in order to realize their gains if they want to use them as collateral. @@ -631,9 +633,11 @@ impl PerpInfo { fn health_contribution(&self, health_type: HealthType) -> I80F48 { let c = self.uncapped_health_contribution(health_type); - // FUTURE: Allow v3-style "reliable" markets where we can return - // `self.quote + weight * self.base` here - c.min(I80F48::ZERO) + if self.trusted_market { + c + } else { + c.min(I80F48::ZERO) + } } #[inline(always)] @@ -837,7 +841,18 @@ impl HealthCache { } } - pub fn spot_health(&self, health_type: HealthType) -> I80F48 { + /// Compute the health when it comes to settling perp pnl + /// + /// Examples: + /// - An account may have maint_health < 0, but settling perp pnl could still be allowed. + /// (+100 USDC health, -50 USDT health, -50 perp health -> allow settling 50 health worth) + /// - Positive health from trusted pnl markets counts + /// - If overall health is 0 with two trusted perp pnl < 0, settling may still be possible. + /// (+100 USDC health, -150 perp1 health, -150 perp2 health -> allow settling 100 health worth) + /// - Positive trusted perp pnl can enable settling. + /// (+100 trusted perp1 health, -100 perp2 health -> allow settling of 100 health worth) + pub fn perp_settle_health(&self) -> I80F48 { + let health_type = HealthType::Maint; let mut health = I80F48::ZERO; for token_info in self.token_infos.iter() { let contrib = token_info.health_contribution(health_type); @@ -847,6 +862,12 @@ impl HealthCache { let contrib = serum3_info.health_contribution(health_type, &self.token_infos); cm!(health += contrib); } + for perp_info in self.perp_infos.iter() { + if perp_info.trusted_market { + let positive_contrib = perp_info.health_contribution(health_type).max(I80F48::ZERO); + cm!(health += positive_contrib); + } + } health } @@ -1988,6 +2009,7 @@ mod tests { quote: I80F48::ZERO, oracle_price: I80F48::from_num(2.0), has_open_orders: false, + trusted_market: false, }; let health_cache = HealthCache { diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index d3cb77f46..12a4bb99b 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -130,6 +130,10 @@ impl PerpMarket { self.group_insurance_fund = if v { 1 } else { 0 }; } + pub fn trusted_market(&self) -> bool { + self.trusted_market == 1 + } + pub fn gen_order_id(&mut self, side: Side, price: i64) -> i128 { self.seq_num += 1; diff --git a/programs/mango-v4/tests/test_liq_perps.rs b/programs/mango-v4/tests/test_liq_perps.rs index 27738595b..693d0039a 100644 --- a/programs/mango-v4/tests/test_liq_perps.rs +++ b/programs/mango-v4/tests/test_liq_perps.rs @@ -526,8 +526,8 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr .await .unwrap(); - let liqee_spot_health_before = 1000.0 + 1.0 * 2.0 * 0.8; - let remaining_pnl = 20.0 * 100.0 - liq_amount_2 + liqee_spot_health_before; + let liqee_settle_health_before = 1000.0 + 1.0 * 2.0 * 0.8; + let remaining_pnl = 20.0 * 100.0 - liq_amount_2 + liqee_settle_health_before; assert!(remaining_pnl < 0.0); let liqee_data = solana.get_account::(account_1).await; assert_eq!(liqee_data.perps[0].base_position_lots(), 0); diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 6bd4d1f54..03b01928f 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -4462,6 +4462,10 @@ export type MangoV4 = { { "name": "hasOpenOrders", "type": "bool" + }, + { + "name": "trustedMarket", + "type": "bool" } ] } @@ -4544,7 +4548,7 @@ export type MangoV4 = { "type": { "array": [ "u8", - 8 + 16 ] } }, @@ -4556,11 +4560,11 @@ export type MangoV4 = { }, { "name": "cumulativeDepositInterest", - "type": "i64" + "type": "f32" }, { "name": "cumulativeBorrowInterest", - "type": "i64" + "type": "f32" } ] } @@ -6070,12 +6074,12 @@ export type MangoV4 = { }, { "name": "cumulativeDepositInterest", - "type": "i64", + "type": "f32", "index": false }, { "name": "cumulativeBorrowInterest", - "type": "i64", + "type": "f32", "index": false } ] @@ -10664,6 +10668,10 @@ export const IDL: MangoV4 = { { "name": "hasOpenOrders", "type": "bool" + }, + { + "name": "trustedMarket", + "type": "bool" } ] } @@ -10746,7 +10754,7 @@ export const IDL: MangoV4 = { "type": { "array": [ "u8", - 8 + 16 ] } }, @@ -10758,11 +10766,11 @@ export const IDL: MangoV4 = { }, { "name": "cumulativeDepositInterest", - "type": "i64" + "type": "f32" }, { "name": "cumulativeBorrowInterest", - "type": "i64" + "type": "f32" } ] } @@ -12272,12 +12280,12 @@ export const IDL: MangoV4 = { }, { "name": "cumulativeDepositInterest", - "type": "i64", + "type": "f32", "index": false }, { "name": "cumulativeBorrowInterest", - "type": "i64", + "type": "f32", "index": false } ] From c22302a1dad45662f02b7065b9b7d5f4652e98ef Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Thu, 29 Sep 2022 15:51:09 +0200 Subject: [PATCH 27/39] ts client improvement (#254) * Perps: Support trusted markets * ts: health on client side Signed-off-by: microwavedcola1 * ts: change perp lookup Signed-off-by: microwavedcola1 * ts: reword error messages, refactor common uses of lookups Signed-off-by: microwavedcola1 * ts: reformat Signed-off-by: microwavedcola1 * ts: improve typing Signed-off-by: microwavedcola1 * ts: fix some todos Signed-off-by: microwavedcola1 * ts: fix some todos Signed-off-by: microwavedcola1 * ts: fixes from review Signed-off-by: microwavedcola1 * ts: type aliasing Signed-off-by: microwavedcola1 * ts: remove '| undefined' where not required as return type Signed-off-by: microwavedcola1 * ts: use trusted market flag for perp health Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 Co-authored-by: Christian Kamm --- .eslintrc.json | 19 +- ts/client/src/accounts/bank.ts | 26 +- ts/client/src/accounts/group.ts | 311 ++++++----- ts/client/src/accounts/healthCache.spec.ts | 369 ++++++++++++- ts/client/src/accounts/healthCache.ts | 418 ++++++++++----- ts/client/src/accounts/mangoAccount.ts | 518 ++++++++++--------- ts/client/src/accounts/oracle.ts | 2 +- ts/client/src/accounts/perp.ts | 65 ++- ts/client/src/accounts/serum3.ts | 57 +- ts/client/src/client.ts | 171 +++--- ts/client/src/debug-scripts/mb-debug-user.ts | 37 +- ts/client/src/ids.ts | 4 +- ts/client/src/scripts/devnet-admin.ts | 1 - ts/client/src/scripts/devnet-user.ts | 112 ++-- ts/client/src/utils.ts | 22 +- ts/client/src/utils/anchor.ts | 42 -- ts/client/src/utils/rpc.ts | 2 +- 17 files changed, 1385 insertions(+), 791 deletions(-) delete mode 100644 ts/client/src/utils/anchor.ts diff --git a/.eslintrc.json b/.eslintrc.json index c525d1fa1..4ad317502 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,12 +14,21 @@ "ecmaVersion": 12, "sourceType": "module" }, - "plugins": ["@typescript-eslint"], + "plugins": [ + "@typescript-eslint" + ], "rules": { - "linebreak-style": ["error", "unix"], - "semi": ["error", "always"], + "linebreak-style": [ + "error", + "unix" + ], + "semi": [ + "error", + "always" + ], "@typescript-eslint/no-non-null-assertion": 0, "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-explicit-any": 0 + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-function-return-type": "warn" } -} +} \ No newline at end of file diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index e5a8d8668..0bd410447 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -1,17 +1,19 @@ import { BN } from '@project-serum/anchor'; import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { PublicKey } from '@solana/web3.js'; -import { nativeI80F48ToUi } from '../utils'; +import { As, nativeI80F48ToUi } from '../utils'; import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48'; export const QUOTE_DECIMALS = 6; +export type TokenIndex = number & As<'token-index'>; + export type OracleConfig = { confFilter: I80F48Dto; }; export interface BankForHealth { - tokenIndex: number; + tokenIndex: TokenIndex; maintAssetWeight: I80F48; initAssetWeight: I80F48; maintLiabWeight: I80F48; @@ -85,7 +87,7 @@ export class Bank implements BankForHealth { mintDecimals: number; bankNum: number; }, - ) { + ): Bank { return new Bank( publicKey, obj.name, @@ -120,7 +122,7 @@ export class Bank implements BankForHealth { obj.dust, obj.flashLoanTokenAccountInitial, obj.flashLoanApprovedAmount, - obj.tokenIndex, + obj.tokenIndex as TokenIndex, obj.mintDecimals, obj.bankNum, ); @@ -160,7 +162,7 @@ export class Bank implements BankForHealth { dust: I80F48Dto, flashLoanTokenAccountInitial: BN, flashLoanApprovedAmount: BN, - public tokenIndex: number, + public tokenIndex: TokenIndex, public mintDecimals: number, public bankNum: number, ) { @@ -207,9 +209,9 @@ export class Bank implements BankForHealth { '\n oracle - ' + this.oracle.toBase58() + '\n price - ' + - this.price?.toNumber() + + this._price?.toNumber() + '\n uiPrice - ' + - this.uiPrice + + this._uiPrice + '\n deposit index - ' + this.depositIndex.toNumber() + '\n borrow index - ' + @@ -268,7 +270,7 @@ export class Bank implements BankForHealth { get price(): I80F48 { if (!this._price) { throw new Error( - `Undefined price for bank ${this.publicKey}, tokenIndex ${this.tokenIndex}`, + `Undefined price for bank ${this.publicKey} with tokenIndex ${this.tokenIndex}!`, ); } return this._price; @@ -277,7 +279,7 @@ export class Bank implements BankForHealth { get uiPrice(): number { if (!this._uiPrice) { throw new Error( - `Undefined uiPrice for bank ${this.publicKey}, tokenIndex ${this.tokenIndex}`, + `Undefined uiPrice for bank ${this.publicKey} with tokenIndex ${this.tokenIndex}!`, ); } return this._uiPrice; @@ -388,11 +390,11 @@ export class MintInfo { registrationTime: BN; groupInsuranceFund: number; }, - ) { + ): MintInfo { return new MintInfo( publicKey, obj.group, - obj.tokenIndex, + obj.tokenIndex as TokenIndex, obj.mint, obj.banks, obj.vaults, @@ -405,7 +407,7 @@ export class MintInfo { constructor( public publicKey: PublicKey, public group: PublicKey, - public tokenIndex: number, + public tokenIndex: TokenIndex, public mint: PublicKey, public banks: PublicKey[], public vaults: PublicKey[], diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 2973a5ed4..9f746a461 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -6,7 +6,7 @@ import { Market, Orderbook, } from '@project-serum/serum'; -import { parsePriceData, PriceData } from '@pythnetwork/client'; +import { parsePriceData } from '@pythnetwork/client'; import { AccountInfo, AddressLookupTableAccount, @@ -17,15 +17,15 @@ import { MangoClient } from '../client'; import { SERUM3_PROGRAM_ID } from '../constants'; import { Id } from '../ids'; import { toNativeDecimals, toUiDecimals } from '../utils'; -import { Bank, MintInfo } from './bank'; +import { Bank, MintInfo, TokenIndex } from './bank'; import { I80F48, ONE_I80F48 } from './I80F48'; import { isPythOracle, isSwitchboardOracle, parseSwitchboardOracle, } from './oracle'; -import { BookSide, PerpMarket } from './perp'; -import { Serum3Market } from './serum3'; +import { BookSide, PerpMarket, PerpMarketIndex } from './perp'; +import { MarketIndex, Serum3Market } from './serum3'; export class Group { static from( @@ -57,12 +57,14 @@ export class Group { new Map(), // banksMapByName new Map(), // banksMapByMint new Map(), // banksMapByTokenIndex - new Map(), // serum3MarketsMap + new Map(), // serum3MarketsMapByExternal + new Map(), // serum3MarketsMapByMarketIndex new Map(), // serum3MarketExternalsMap - new Map(), // perpMarketsMap + new Map(), // perpMarketsMapByOracle + new Map(), // perpMarketsMapByMarketIndex + new Map(), // perpMarketsMapByName new Map(), // mintInfosMapByTokenIndex new Map(), // mintInfosMapByMint - new Map(), // oraclesMap new Map(), // vaultAmountsMap ); } @@ -81,18 +83,19 @@ export class Group { public addressLookupTablesList: AddressLookupTableAccount[], public banksMapByName: Map, public banksMapByMint: Map, - public banksMapByTokenIndex: Map, + public banksMapByTokenIndex: Map, public serum3MarketsMapByExternal: Map, - public serum3MarketExternalsMap: Map, - // TODO rethink key - public perpMarketsMap: Map, - public mintInfosMapByTokenIndex: Map, + public serum3MarketsMapByMarketIndex: Map, + public serum3ExternalMarketsMap: Map, + public perpMarketsMapByOracle: Map, + public perpMarketsMapByMarketIndex: Map, + public perpMarketsMapByName: Map, + public mintInfosMapByTokenIndex: Map, public mintInfosMapByMint: Map, - private oraclesMap: Map, // UNUSED public vaultAmountsMap: Map, ) {} - public async reloadAll(client: MangoClient) { + public async reloadAll(client: MangoClient): Promise { let ids: Id | undefined = undefined; if (client.idsSource === 'api') { @@ -109,12 +112,12 @@ export class Group { this.reloadBanks(client, ids).then(() => Promise.all([ this.reloadBankOraclePrices(client), - this.reloadVaults(client, ids), + this.reloadVaults(client), ]), ), this.reloadMintInfos(client, ids), this.reloadSerum3Markets(client, ids).then(() => - this.reloadSerum3ExternalMarkets(client, ids), + this.reloadSerum3ExternalMarkets(client), ), this.reloadPerpMarkets(client, ids).then(() => this.reloadPerpMarketOraclePrices(client), @@ -123,7 +126,7 @@ export class Group { // console.timeEnd('group.reload'); } - public async reloadAlts(client: MangoClient) { + public async reloadAlts(client: MangoClient): Promise { const alts = await Promise.all( this.addressLookupTables .filter((alt) => !alt.equals(PublicKey.default)) @@ -133,13 +136,13 @@ export class Group { ); this.addressLookupTablesList = alts.map((res, i) => { if (!res || !res.value) { - throw new Error(`Error in getting ALT ${this.addressLookupTables[i]}`); + throw new Error(`Undefined ALT ${this.addressLookupTables[i]}!`); } return res.value; }); } - public async reloadBanks(client: MangoClient, ids?: Id) { + public async reloadBanks(client: MangoClient, ids?: Id): Promise { let banks: Bank[]; if (ids && ids.getBanks().length) { @@ -169,7 +172,7 @@ export class Group { } } - public async reloadMintInfos(client: MangoClient, ids?: Id) { + public async reloadMintInfos(client: MangoClient, ids?: Id): Promise { let mintInfos: MintInfo[]; if (ids && ids.getMintInfos().length) { mintInfos = ( @@ -194,7 +197,10 @@ export class Group { ); } - public async reloadSerum3Markets(client: MangoClient, ids?: Id) { + public async reloadSerum3Markets( + client: MangoClient, + ids?: Id, + ): Promise { let serum3Markets: Serum3Market[]; if (ids && ids.getSerum3Markets().length) { serum3Markets = ( @@ -214,9 +220,15 @@ export class Group { serum3Market, ]), ); + this.serum3MarketsMapByMarketIndex = new Map( + serum3Markets.map((serum3Market) => [ + serum3Market.marketIndex, + serum3Market, + ]), + ); } - public async reloadSerum3ExternalMarkets(client: MangoClient, ids?: Id) { + public async reloadSerum3ExternalMarkets(client: MangoClient): Promise { const externalMarkets = await Promise.all( Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) => Market.load( @@ -228,7 +240,7 @@ export class Group { ), ); - this.serum3MarketExternalsMap = new Map( + this.serum3ExternalMarketsMap = new Map( Array.from(this.serum3MarketsMapByExternal.values()).map( (serum3Market, index) => [ serum3Market.serumMarketExternal.toBase58(), @@ -238,7 +250,7 @@ export class Group { ); } - public async reloadPerpMarkets(client: MangoClient, ids?: Id) { + public async reloadPerpMarkets(client: MangoClient, ids?: Id): Promise { let perpMarkets: PerpMarket[]; if (ids && ids.getPerpMarkets().length) { perpMarkets = ( @@ -252,9 +264,18 @@ export class Group { perpMarkets = await client.perpGetMarkets(this); } - this.perpMarketsMap = new Map( + this.perpMarketsMapByName = new Map( perpMarkets.map((perpMarket) => [perpMarket.name, perpMarket]), ); + this.perpMarketsMapByOracle = new Map( + perpMarkets.map((perpMarket) => [ + perpMarket.oracle.toBase58(), + perpMarket, + ]), + ); + this.perpMarketsMapByMarketIndex = new Map( + perpMarkets.map((perpMarket) => [perpMarket.perpMarketIndex, perpMarket]), + ); } public async reloadBankOraclePrices(client: MangoClient): Promise { @@ -293,7 +314,9 @@ export class Group { public async reloadPerpMarketOraclePrices( client: MangoClient, ): Promise { - const perpMarkets: PerpMarket[] = Array.from(this.perpMarketsMap.values()); + const perpMarkets: PerpMarket[] = Array.from( + this.perpMarketsMapByName.values(), + ); const oracles = perpMarkets.map((b) => b.oracle); const ais = await client.program.provider.connection.getMultipleAccountsInfo(oracles); @@ -302,15 +325,17 @@ export class Group { ais.forEach(async (ai, i) => { const perpMarket = perpMarkets[i]; if (!ai) - throw new Error('Undefined ai object in reloadPerpMarketOraclePrices!'); + throw new Error( + `Undefined ai object in reloadPerpMarketOraclePrices for ${perpMarket.oracle}!`, + ); const { price, uiPrice } = await this.decodePriceFromOracleAi( coder, perpMarket.oracle, ai, perpMarket.baseDecimals, ); - perpMarket.price = price; - perpMarket.uiPrice = uiPrice; + perpMarket._price = price; + perpMarket._uiPrice = uiPrice; }); } @@ -319,7 +344,7 @@ export class Group { oracle: PublicKey, ai: AccountInfo, baseDecimals: number, - ) { + ): Promise<{ price: I80F48; uiPrice: number }> { let price, uiPrice; if ( !BorshAccountsCoder.accountDiscriminator('stubOracle').compare( @@ -337,13 +362,13 @@ export class Group { price = this?.toNativePrice(uiPrice, baseDecimals); } else { throw new Error( - `Unknown oracle provider for oracle ${oracle}, with owner ${ai.owner}`, + `Unknown oracle provider (parsing not implemented) for oracle ${oracle}, with owner ${ai.owner}!`, ); } return { price, uiPrice }; } - public async reloadVaults(client: MangoClient, ids?: Id): Promise { + public async reloadVaults(client: MangoClient): Promise { const vaultPks = Array.from(this.banksMapByMint.values()) .flat() .map((bank) => bank.vault); @@ -354,7 +379,9 @@ export class Group { this.vaultAmountsMap = new Map( vaultAccounts.map((vaultAi, i) => { - if (!vaultAi) throw new Error('Missing vault account info'); + if (!vaultAi) { + throw new Error(`Undefined vaultAi for ${vaultPks[i]}`!); + } const vaultAmount = coder() .accounts.decode('token', vaultAi.data) .amount.toNumber(); @@ -365,8 +392,7 @@ export class Group { public getMintDecimals(mintPk: PublicKey): number { const banks = this.banksMapByMint.get(mintPk.toString()); - if (!banks) - throw new Error(`Unable to find mint decimals for ${mintPk.toString()}`); + if (!banks) throw new Error(`No bank found for mint ${mintPk}!`); return banks[0].mintDecimals; } @@ -376,14 +402,13 @@ export class Group { public getFirstBankByMint(mintPk: PublicKey): Bank { const banks = this.banksMapByMint.get(mintPk.toString()); - if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`); + if (!banks) throw new Error(`No bank found for mint ${mintPk}!`); return banks[0]; } - public getFirstBankByTokenIndex(tokenIndex: number): Bank { + public getFirstBankByTokenIndex(tokenIndex: TokenIndex): Bank { const banks = this.banksMapByTokenIndex.get(tokenIndex); - if (!banks) - throw new Error(`Unable to find banks for tokenIndex ${tokenIndex}`); + if (!banks) throw new Error(`No bank found for tokenIndex ${tokenIndex}!`); return banks[0]; } @@ -394,10 +419,7 @@ export class Group { */ public getTokenVaultBalanceByMint(mintPk: PublicKey): I80F48 { const banks = this.banksMapByMint.get(mintPk.toBase58()); - if (!banks) - throw new Error( - `Mint does not exist in getTokenVaultBalanceByMint ${mintPk.toString()}`, - ); + if (!banks) throw new Error(`No bank found for mint ${mintPk}!`); let totalAmount = 0; for (const bank of banks) { const amount = this.vaultAmountsMap.get(bank.vault.toBase58()); @@ -408,83 +430,6 @@ export class Group { return I80F48.fromNumber(totalAmount); } - public findPerpMarket(marketIndex: number): PerpMarket | undefined { - return Array.from(this.perpMarketsMap.values()).find( - (perpMarket) => perpMarket.perpMarketIndex === marketIndex, - ); - } - - public findSerum3Market(marketIndex: number): Serum3Market | undefined { - return Array.from(this.serum3MarketsMapByExternal.values()).find( - (serum3Market) => serum3Market.marketIndex === marketIndex, - ); - } - - public findSerum3MarketByName(name: string): Serum3Market | undefined { - return Array.from(this.serum3MarketsMapByExternal.values()).find( - (serum3Market) => serum3Market.name === name, - ); - } - - public async loadSerum3BidsForMarket( - client: MangoClient, - externalMarketPk: PublicKey, - ): Promise { - const serum3Market = this.serum3MarketsMapByExternal.get( - externalMarketPk.toBase58(), - ); - if (!serum3Market) { - throw new Error( - `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, - ); - } - return await serum3Market.loadBids(client, this); - } - - public async loadSerum3AsksForMarket( - client: MangoClient, - externalMarketPk: PublicKey, - ): Promise { - const serum3Market = this.serum3MarketsMapByExternal.get( - externalMarketPk.toBase58(), - ); - if (!serum3Market) { - throw new Error( - `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, - ); - } - return await serum3Market.loadAsks(client, this); - } - - public getFeeRate(maker = true) { - // TODO: fetch msrm/srm vault balance - const feeTier = getFeeTier(0, 0); - const rates = getFeeRates(feeTier); - return maker ? rates.maker : rates.taker; - } - - public async loadPerpBidsForMarket( - client: MangoClient, - marketName: string, - ): Promise { - const perpMarket = this.perpMarketsMap.get(marketName); - if (!perpMarket) { - throw new Error(`Perp Market ${marketName} not found!`); - } - return await perpMarket.loadBids(client); - } - - public async loadPerpAsksForMarket( - client: MangoClient, - marketName: string, - ): Promise { - const perpMarket = this.perpMarketsMap.get(marketName); - if (!perpMarket) { - throw new Error(`Perp Market ${marketName} not found!`); - } - return await perpMarket.loadAsks(client); - } - /** * * @param mintPk @@ -497,7 +442,131 @@ export class Group { return toUiDecimals(vaultBalance, mintDecimals); } - public consoleLogBanks() { + public getSerum3MarketByMarketIndex(marketIndex: MarketIndex): Serum3Market { + const serum3Market = this.serum3MarketsMapByMarketIndex.get(marketIndex); + if (!serum3Market) { + throw new Error(`No serum3Market found for marketIndex ${marketIndex}!`); + } + return serum3Market; + } + + public getSerum3MarketByName(name: string): Serum3Market { + const serum3Market = Array.from( + this.serum3MarketsMapByExternal.values(), + ).find((serum3Market) => serum3Market.name === name); + if (!serum3Market) { + throw new Error(`No serum3Market found by name ${name}!`); + } + return serum3Market; + } + + public getSerum3MarketByExternalMarket( + externalMarketPk: PublicKey, + ): Serum3Market { + const serum3Market = Array.from( + this.serum3MarketsMapByExternal.values(), + ).find((serum3Market) => + serum3Market.serumMarketExternal.equals(externalMarketPk), + ); + if (!serum3Market) { + throw new Error( + `No serum3Market found for external serum3 market ${externalMarketPk.toString()}!`, + ); + } + return serum3Market; + } + + public getSerum3ExternalMarket(externalMarketPk: PublicKey): Market { + const market = this.serum3ExternalMarketsMap.get( + externalMarketPk.toBase58(), + ); + if (!market) { + throw new Error( + `No external market found for pk ${externalMarketPk.toString()}!`, + ); + } + return market; + } + + public async loadSerum3BidsForMarket( + client: MangoClient, + externalMarketPk: PublicKey, + ): Promise { + const serum3Market = this.getSerum3MarketByExternalMarket(externalMarketPk); + return await serum3Market.loadBids(client, this); + } + + public async loadSerum3AsksForMarket( + client: MangoClient, + externalMarketPk: PublicKey, + ): Promise { + const serum3Market = this.getSerum3MarketByExternalMarket(externalMarketPk); + return await serum3Market.loadAsks(client, this); + } + + public getSerum3FeeRates(maker = true): number { + // TODO: fetch msrm/srm vault balance + const feeTier = getFeeTier(0, 0); + const rates = getFeeRates(feeTier); + return maker ? rates.maker : rates.taker; + } + + public findPerpMarket(marketIndex: PerpMarketIndex): PerpMarket { + const perpMarket = Array.from(this.perpMarketsMapByName.values()).find( + (perpMarket) => perpMarket.perpMarketIndex === marketIndex, + ); + if (!perpMarket) { + throw new Error( + `No perpMarket found for perpMarketIndex ${marketIndex}!`, + ); + } + return perpMarket; + } + + public getPerpMarketByOracle(oracle: PublicKey): PerpMarket { + const perpMarket = this.perpMarketsMapByOracle.get(oracle.toBase58()); + if (!perpMarket) { + throw new Error(`No PerpMarket found for oracle ${oracle}!`); + } + return perpMarket; + } + + public getPerpMarketByMarketIndex(marketIndex: PerpMarketIndex): PerpMarket { + const perpMarket = this.perpMarketsMapByMarketIndex.get(marketIndex); + if (!perpMarket) { + throw new Error(`No PerpMarket found with marketIndex ${marketIndex}!`); + } + return perpMarket; + } + + public getPerpMarketByName(perpMarketName: string): PerpMarket { + const perpMarket = Array.from( + this.perpMarketsMapByMarketIndex.values(), + ).find((perpMarket) => perpMarket.name === perpMarketName); + if (!perpMarket) { + throw new Error(`No PerpMarket found by name ${perpMarketName}!`); + } + return perpMarket; + } + + public async loadPerpBidsForMarket( + client: MangoClient, + perpMarketIndex: PerpMarketIndex, + ): Promise { + const perpMarket = this.getPerpMarketByMarketIndex(perpMarketIndex); + return await perpMarket.loadBids(client); + } + + public async loadPerpAsksForMarket( + client: MangoClient, + group: Group, + perpMarketIndex: PerpMarketIndex, + ): Promise { + const perpMarket = this.getPerpMarketByMarketIndex(perpMarketIndex); + return await perpMarket.loadAsks(client); + } + + public consoleLogBanks(): void { for (const mintBanks of this.banksMapByMint.values()) { for (const bank of mintBanks) { console.log(bank.toString()); diff --git a/ts/client/src/accounts/healthCache.spec.ts b/ts/client/src/accounts/healthCache.spec.ts index 5d1697083..e0beb5252 100644 --- a/ts/client/src/accounts/healthCache.spec.ts +++ b/ts/client/src/accounts/healthCache.spec.ts @@ -1,14 +1,371 @@ +import { BN } from '@project-serum/anchor'; +import { OpenOrders } from '@project-serum/serum'; import { expect } from 'chai'; import { toUiDecimalsForQuote } from '../utils'; -import { BankForHealth } from './bank'; -import { HealthCache, TokenInfo } from './healthCache'; +import { BankForHealth, TokenIndex } from './bank'; +import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache'; import { I80F48, ZERO_I80F48 } from './I80F48'; +import { HealthType, PerpPosition } from './mangoAccount'; +import { PerpMarket } from './perp'; +import { MarketIndex } from './serum3'; + +function mockBankAndOracle( + tokenIndex: TokenIndex, + maintWeight: number, + initWeight: number, + price: number, +): BankForHealth { + return { + tokenIndex, + maintAssetWeight: I80F48.fromNumber(1 - maintWeight), + initAssetWeight: I80F48.fromNumber(1 - initWeight), + maintLiabWeight: I80F48.fromNumber(1 + maintWeight), + initLiabWeight: I80F48.fromNumber(1 + initWeight), + price: I80F48.fromNumber(price), + }; +} + +function mockPerpMarket( + perpMarketIndex: number, + maintWeight: number, + initWeight: number, + price: I80F48, +): PerpMarket { + return { + perpMarketIndex, + maintAssetWeight: I80F48.fromNumber(1 - maintWeight), + initAssetWeight: I80F48.fromNumber(1 - initWeight), + maintLiabWeight: I80F48.fromNumber(1 + maintWeight), + initLiabWeight: I80F48.fromNumber(1 + initWeight), + price, + quoteLotSize: new BN(100), + baseLotSize: new BN(10), + longFunding: ZERO_I80F48(), + shortFunding: ZERO_I80F48(), + } as unknown as PerpMarket; +} describe('Health Cache', () => { + it('test_health0', () => { + const sourceBank: BankForHealth = mockBankAndOracle( + 1 as TokenIndex, + 0.1, + 0.2, + 1, + ); + const targetBank: BankForHealth = mockBankAndOracle( + 4 as TokenIndex, + 0.3, + 0.5, + 5, + ); + + const ti1 = TokenInfo.fromBank(sourceBank, I80F48.fromNumber(100)); + const ti2 = TokenInfo.fromBank(targetBank, I80F48.fromNumber(-10)); + + const si1 = Serum3Info.fromOoModifyingTokenInfos( + 1, + ti2, + 0, + ti1, + 2 as MarketIndex, + { + quoteTokenTotal: new BN(21), + baseTokenTotal: new BN(18), + quoteTokenFree: new BN(1), + baseTokenFree: new BN(3), + referrerRebatesAccrued: new BN(2), + } as any as OpenOrders, + ); + + const pM = mockPerpMarket(9, 0.1, 0.2, targetBank.price); + const pp = new PerpPosition( + pM.perpMarketIndex, + 3, + I80F48.fromNumber(-310), + 7, + 11, + 1, + 2, + I80F48.fromNumber(0), + I80F48.fromNumber(0), + ); + const pi1 = PerpInfo.fromPerpPosition(pM, pp); + + const hc = new HealthCache([ti1, ti2], [si1], [pi1]); + + // for bank1/oracle1, including open orders (scenario: bids execute) + const health1 = (100.0 + 1.0 + 2.0 + (20.0 + 15.0 * 5.0)) * 0.8; + // for bank2/oracle2 + const health2 = (-10.0 + 3.0) * 5.0 * 1.5; + // for perp (scenario: bids execute) + const health3 = + (3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 + + (-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0); + + const health = hc.health(HealthType.init).toNumber(); + console.log( + `health ${health + .toFixed(3) + .padStart( + 10, + )}, case "test that includes all the side values (like referrer_rebates_accrued)"`, + ); + + expect(health - (health1 + health2 + health3)).lessThan(0.0000001); + }); + + it('test_health1', () => { + function testFixture(fixture: { + name: string; + token1: number; + token2: number; + token3: number; + oo12: [number, number]; + oo13: [number, number]; + perp1: [number, number, number, number]; + expectedHealth: number; + }): void { + const bank1: BankForHealth = mockBankAndOracle( + 1 as TokenIndex, + 0.1, + 0.2, + 1, + ); + const bank2: BankForHealth = mockBankAndOracle( + 4 as TokenIndex, + 0.3, + 0.5, + 5, + ); + const bank3: BankForHealth = mockBankAndOracle( + 5 as TokenIndex, + 0.3, + 0.5, + 10, + ); + + const ti1 = TokenInfo.fromBank(bank1, I80F48.fromNumber(fixture.token1)); + const ti2 = TokenInfo.fromBank(bank2, I80F48.fromNumber(fixture.token2)); + const ti3 = TokenInfo.fromBank(bank3, I80F48.fromNumber(fixture.token3)); + + const si1 = Serum3Info.fromOoModifyingTokenInfos( + 1, + ti2, + 0, + ti1, + 2 as MarketIndex, + { + quoteTokenTotal: new BN(fixture.oo12[0]), + baseTokenTotal: new BN(fixture.oo12[1]), + quoteTokenFree: new BN(0), + baseTokenFree: new BN(0), + referrerRebatesAccrued: new BN(0), + } as any as OpenOrders, + ); + + const si2 = Serum3Info.fromOoModifyingTokenInfos( + 2, + ti3, + 0, + ti1, + 2 as MarketIndex, + { + quoteTokenTotal: new BN(fixture.oo13[0]), + baseTokenTotal: new BN(fixture.oo13[1]), + quoteTokenFree: new BN(0), + baseTokenFree: new BN(0), + referrerRebatesAccrued: new BN(0), + } as any as OpenOrders, + ); + + const pM = mockPerpMarket(9, 0.1, 0.2, bank2.price); + const pp = new PerpPosition( + pM.perpMarketIndex, + fixture.perp1[0], + I80F48.fromNumber(fixture.perp1[1]), + fixture.perp1[2], + fixture.perp1[3], + 0, + 0, + I80F48.fromNumber(0), + I80F48.fromNumber(0), + ); + const pi1 = PerpInfo.fromPerpPosition(pM, pp); + + const hc = new HealthCache([ti1, ti2, ti3], [si1, si2], [pi1]); + const health = hc.health(HealthType.init).toNumber(); + console.log( + `health ${health.toFixed(3).padStart(10)}, case "${fixture.name}"`, + ); + expect(health - fixture.expectedHealth).lessThan(0.0000001); + } + + const basePrice = 5; + const baseLotsToQuote = 10.0 * basePrice; + + testFixture({ + name: '0', + token1: 100, + token2: -10, + token3: 0, + oo12: [20, 15], + oo13: [0, 0], + perp1: [3, -131, 7, 11], + expectedHealth: + // for token1, including open orders (scenario: bids execute) + (100.0 + (20.0 + 15.0 * basePrice)) * 0.8 - + // for token2 + 10.0 * basePrice * 1.5 + + // for perp (scenario: bids execute) + (3.0 + 7.0) * baseLotsToQuote * 0.8 + + (-131.0 - 7.0 * baseLotsToQuote), + }); + + testFixture({ + name: '1', + token1: -100, + token2: 10, + token3: 0, + oo12: [20, 15], + oo13: [0, 0], + perp1: [-10, -131, 7, 11], + expectedHealth: + // for token1 + -100.0 * 1.2 + + // for token2, including open orders (scenario: asks execute) + (10.0 * basePrice + (20.0 + 15.0 * basePrice)) * 0.5 + + // for perp (scenario: asks execute) + (-10.0 - 11.0) * baseLotsToQuote * 1.2 + + (-131.0 + 11.0 * baseLotsToQuote), + }); + + testFixture({ + name: '2', + token1: 0, + token2: 0, + token3: 0, + oo12: [0, 0], + oo13: [0, 0], + perp1: [-10, 100, 0, 0], + expectedHealth: 0, + }); + + testFixture({ + name: '3', + token1: 0, + token2: 0, + token3: 0, + oo12: [0, 0], + oo13: [0, 0], + perp1: [1, -100, 0, 0], + expectedHealth: -100.0 + 0.8 * 1.0 * baseLotsToQuote, + }); + + testFixture({ + name: '4', + token1: 0, + token2: 0, + token3: 0, + oo12: [0, 0], + oo13: [0, 0], + perp1: [10, 100, 0, 0], + expectedHealth: 0, + }); + + testFixture({ + name: '5', + token1: 0, + token2: 0, + token3: 0, + oo12: [0, 0], + oo13: [0, 0], + perp1: [30, -100, 0, 0], + expectedHealth: 0, + }); + + testFixture({ + name: '6, reserved oo funds', + token1: -100, + token2: -10, + token3: -10, + oo12: [1, 1], + oo13: [1, 1], + perp1: [30, -100, 0, 0], + expectedHealth: + // tokens + -100.0 * 1.2 - + 10.0 * 5.0 * 1.5 - + 10.0 * 10.0 * 1.5 + + // oo_1_2 (-> token1) + (1.0 + 5.0) * 1.2 + + // oo_1_3 (-> token1) + (1.0 + 10.0) * 1.2, + }); + + testFixture({ + name: '7, reserved oo funds cross the zero balance level', + token1: -14, + token2: -10, + token3: -10, + oo12: [1, 1], + oo13: [1, 1], + perp1: [0, 0, 0, 0], + expectedHealth: + -14.0 * 1.2 - + 10.0 * 5.0 * 1.5 - + 10.0 * 10.0 * 1.5 + + // oo_1_2 (-> token1) + 3.0 * 1.2 + + 3.0 * 0.8 + + // oo_1_3 (-> token1) + 8.0 * 1.2 + + 3.0 * 0.8, + }); + + testFixture({ + name: '8, reserved oo funds in a non-quote currency', + token1: -100, + token2: -100, + token3: -1, + oo12: [0, 0], + oo13: [10, 1], + perp1: [0, 0, 0, 0], + expectedHealth: + // tokens + -100.0 * 1.2 - + 100.0 * 5.0 * 1.5 - + 10.0 * 1.5 + + // oo_1_3 (-> token3) + 10.0 * 1.5 + + 10.0 * 0.5, + }); + + testFixture({ + name: '9, like 8 but oo_1_2 flips the oo_1_3 target', + token1: -100, + token2: -100, + token3: -1, + oo12: [100, 0], + oo13: [10, 1], + perp1: [0, 0, 0, 0], + expectedHealth: + // tokens + -100.0 * 1.2 - + 100.0 * 5.0 * 1.5 - + 10.0 * 1.5 + + // oo_1_2 (-> token1) + 80.0 * 1.2 + + 20.0 * 0.8 + + // oo_1_3 (-> token1) + 20.0 * 0.8, + }); + }); + it('max swap tokens for min ratio', () => { // USDC like const sourceBank: BankForHealth = { - tokenIndex: 0, + tokenIndex: 0 as TokenIndex, maintAssetWeight: I80F48.fromNumber(1), initAssetWeight: I80F48.fromNumber(1), maintLiabWeight: I80F48.fromNumber(1), @@ -17,7 +374,7 @@ describe('Health Cache', () => { }; // BTC like const targetBank: BankForHealth = { - tokenIndex: 1, + tokenIndex: 1 as TokenIndex, maintAssetWeight: I80F48.fromNumber(0.9), initAssetWeight: I80F48.fromNumber(0.8), maintLiabWeight: I80F48.fromNumber(1.1), @@ -28,7 +385,7 @@ describe('Health Cache', () => { const hc = new HealthCache( [ new TokenInfo( - 0, + 0 as TokenIndex, sourceBank.maintAssetWeight, sourceBank.initAssetWeight, sourceBank.maintLiabWeight, @@ -39,7 +396,7 @@ describe('Health Cache', () => { ), new TokenInfo( - 1, + 1 as TokenIndex, targetBank.maintAssetWeight, targetBank.initAssetWeight, targetBank.maintLiabWeight, diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index c7444cfed..b06477ab7 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -1,18 +1,20 @@ +import { BN } from '@project-serum/anchor'; +import { OpenOrders } from '@project-serum/serum'; import { PublicKey } from '@solana/web3.js'; import _ from 'lodash'; -import { Bank, BankForHealth } from './bank'; +import { Bank, BankForHealth, TokenIndex } from './bank'; import { Group } from './group'; import { HUNDRED_I80F48, I80F48, I80F48Dto, MAX_I80F48, - ONE_I80F48, ZERO_I80F48, } from './I80F48'; -import { HealthType } from './mangoAccount'; + +import { HealthType, MangoAccount, PerpPosition } from './mangoAccount'; import { PerpMarket, PerpOrderSide } from './perp'; -import { Serum3Market, Serum3Side } from './serum3'; +import { MarketIndex, Serum3Market, Serum3Side } from './serum3'; // ░░░░ // @@ -45,7 +47,63 @@ export class HealthCache { public perpInfos: PerpInfo[], ) {} - static fromDto(dto) { + static fromMangoAccount( + group: Group, + mangoAccount: MangoAccount, + ): HealthCache { + // token contribution from token accounts + const tokenInfos = mangoAccount.tokensActive().map((tokenPosition) => { + const bank = group.getFirstBankByTokenIndex(tokenPosition.tokenIndex); + return TokenInfo.fromBank(bank, tokenPosition.balance(bank)); + }); + + // Fill the TokenInfo balance with free funds in serum3 oo accounts, and fill + // the serum3MaxReserved with their reserved funds. Also build Serum3Infos. + const serum3Infos = mangoAccount.serum3Active().map((serum3) => { + const oo = mangoAccount.getSerum3OoAccount(serum3.marketIndex); + + // find the TokenInfos for the market's base and quote tokens + const baseIndex = tokenInfos.findIndex( + (tokenInfo) => tokenInfo.tokenIndex === serum3.baseTokenIndex, + ); + const baseInfo = tokenInfos[baseIndex]; + if (!baseInfo) { + throw new Error( + `BaseInfo not found for market with marketIndex ${serum3.marketIndex}!`, + ); + } + const quoteIndex = tokenInfos.findIndex( + (tokenInfo) => tokenInfo.tokenIndex === serum3.quoteTokenIndex, + ); + const quoteInfo = tokenInfos[quoteIndex]; + if (!quoteInfo) { + throw new Error( + `QuoteInfo not found for market with marketIndex ${serum3.marketIndex}!`, + ); + } + + return Serum3Info.fromOoModifyingTokenInfos( + baseIndex, + baseInfo, + quoteIndex, + quoteInfo, + serum3.marketIndex, + oo, + ); + }); + + // health contribution from perp accounts + const perpInfos = mangoAccount.perpActive().map((perpPosition) => { + const perpMarket = group.getPerpMarketByMarketIndex( + perpPosition.marketIndex, + ); + return PerpInfo.fromPerpPosition(perpMarket, perpPosition); + }); + + return new HealthCache(tokenInfos, serum3Infos, perpInfos); + } + + static fromDto(dto): HealthCache { return new HealthCache( dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)), dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)), @@ -57,6 +115,7 @@ export class HealthCache { const health = ZERO_I80F48(); for (const tokenInfo of this.tokenInfos) { const contrib = tokenInfo.healthContribution(healthType); + // console.log(` - ti ${contrib}`); health.iadd(contrib); } for (const serum3Info of this.serum3Infos) { @@ -64,10 +123,12 @@ export class HealthCache { healthType, this.tokenInfos, ); + // console.log(` - si ${contrib}`); health.iadd(contrib); } for (const perpInfo of this.perpInfos) { const contrib = perpInfo.healthContribution(healthType); + // console.log(` - pi ${contrib}`); health.iadd(contrib); } return health; @@ -164,34 +225,32 @@ export class HealthCache { } } - findTokenInfoIndex(tokenIndex: number): number { + findTokenInfoIndex(tokenIndex: TokenIndex): number { return this.tokenInfos.findIndex( - (tokenInfo) => tokenInfo.tokenIndex == tokenIndex, + (tokenInfo) => tokenInfo.tokenIndex === tokenIndex, ); } getOrCreateTokenInfoIndex(bank: BankForHealth): number { const index = this.findTokenInfoIndex(bank.tokenIndex); if (index == -1) { - this.tokenInfos.push(TokenInfo.emptyFromBank(bank)); + this.tokenInfos.push(TokenInfo.fromBank(bank)); } return this.findTokenInfoIndex(bank.tokenIndex); } - findSerum3InfoIndex(marketIndex: number): number { + findSerum3InfoIndex(marketIndex: MarketIndex): number { return this.serum3Infos.findIndex( (serum3Info) => serum3Info.marketIndex === marketIndex, ); } - getOrCreateSerum3InfoIndex(group: Group, serum3Market: Serum3Market): number { + getOrCreateSerum3InfoIndex( + baseBank: BankForHealth, + quoteBank: BankForHealth, + serum3Market: Serum3Market, + ): number { const index = this.findSerum3InfoIndex(serum3Market.marketIndex); - const baseBank = group.getFirstBankByTokenIndex( - serum3Market.baseTokenIndex, - ); - const quoteBank = group.getFirstBankByTokenIndex( - serum3Market.quoteTokenIndex, - ); const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank); const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank); if (index == -1) { @@ -208,20 +267,14 @@ export class HealthCache { adjustSerum3Reserved( // todo change indices to types from numbers - group: Group, + baseBank: BankForHealth, + quoteBank: BankForHealth, serum3Market: Serum3Market, reservedBaseChange: I80F48, freeBaseChange: I80F48, reservedQuoteChange: I80F48, freeQuoteChange: I80F48, - ) { - const baseBank = group.getFirstBankByTokenIndex( - serum3Market.baseTokenIndex, - ); - const quoteBank = group.getFirstBankByTokenIndex( - serum3Market.quoteTokenIndex, - ); - + ): void { const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank); const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank); @@ -238,7 +291,11 @@ export class HealthCache { quoteEntry.balance.iadd(freeQuoteChange.mul(quoteEntry.oraclePrice)); // Apply it to the serum3 info - const index = this.getOrCreateSerum3InfoIndex(group, serum3Market); + const index = this.getOrCreateSerum3InfoIndex( + baseBank, + quoteBank, + serum3Market, + ); const serum3Info = this.serum3Infos[index]; serum3Info.reserved = serum3Info.reserved.add(reservedAmount); } @@ -257,7 +314,7 @@ export class HealthCache { return this.findPerpInfoIndex(perpMarket.perpMarketIndex); } - public static logHealthCache(debug: string, healthCache: HealthCache) { + public static logHealthCache(debug: string, healthCache: HealthCache): void { if (debug) console.log(debug); for (const token of healthCache.tokenInfos) { console.log(` ${token.toString()}`); @@ -293,10 +350,6 @@ export class HealthCache { for (const change of nativeTokenChanges) { const bank: Bank = group.getFirstBankByMint(change.mintPk); const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank); - if (!bank.price) - throw new Error( - `Oracle price not loaded for ${change.mintPk.toString()}`, - ); adjustedCache.tokenInfos[changeIndex].balance.iadd( change.nativeTokenAmount.mul(bank.price), ); @@ -306,18 +359,13 @@ export class HealthCache { } simHealthRatioWithSerum3BidChanges( - group: Group, + baseBank: BankForHealth, + quoteBank: BankForHealth, bidNativeQuoteAmount: I80F48, serum3Market: Serum3Market, healthType: HealthType = HealthType.init, ): I80F48 { const adjustedCache: HealthCache = _.cloneDeep(this); - const quoteBank = group.getFirstBankByTokenIndex( - serum3Market.quoteTokenIndex, - ); - if (!quoteBank) { - throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`); - } const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank); const quote = adjustedCache.tokenInfos[quoteIndex]; @@ -331,7 +379,8 @@ export class HealthCache { // Increase reserved in Serum3Info for quote adjustedCache.adjustSerum3Reserved( - group, + baseBank, + quoteBank, serum3Market, ZERO_I80F48(), ZERO_I80F48(), @@ -342,18 +391,13 @@ export class HealthCache { } simHealthRatioWithSerum3AskChanges( - group: Group, + baseBank: BankForHealth, + quoteBank: BankForHealth, askNativeBaseAmount: I80F48, serum3Market: Serum3Market, healthType: HealthType = HealthType.init, ): I80F48 { const adjustedCache: HealthCache = _.cloneDeep(this); - const baseBank = group.getFirstBankByTokenIndex( - serum3Market.baseTokenIndex, - ); - if (!baseBank) { - throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`); - } const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank); const base = adjustedCache.tokenInfos[baseIndex]; @@ -367,7 +411,8 @@ export class HealthCache { // Increase reserved in Serum3Info for base adjustedCache.adjustSerum3Reserved( - group, + baseBank, + quoteBank, serum3Market, askNativeBaseAmount, ZERO_I80F48(), @@ -384,7 +429,7 @@ export class HealthCache { rightRatio: I80F48, targetRatio: I80F48, healthRatioAfterActionFn: (I80F48) => I80F48, - ) { + ): I80F48 { const maxIterations = 40; // TODO: make relative to health ratio decimals? Might be over engineering const targetError = I80F48.fromNumber(0.001); @@ -396,11 +441,12 @@ export class HealthCache { rightRatio.sub(targetRatio).isNeg()) ) { throw new Error( - `internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`, + `Internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}, likely reason is the zeroAmount not been tight enough!`, ); } let newAmount; + // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const key of Array(maxIterations).fill(0).keys()) { newAmount = left.add(right).mul(I80F48.fromNumber(0.5)); const newAmountRatio = healthRatioAfterActionFn(newAmount); @@ -427,15 +473,6 @@ export class HealthCache { minRatio: I80F48, priceFactor: I80F48, ): I80F48 { - if ( - !sourceBank.price || - sourceBank.price.lte(ZERO_I80F48()) || - !targetBank.price || - targetBank.price.lte(ZERO_I80F48()) - ) { - return ZERO_I80F48(); - } - if ( sourceBank.initLiabWeight .sub(targetBank.initAssetWeight) @@ -454,6 +491,7 @@ export class HealthCache { // - be careful about finding the minRatio point: the function isn't convex const initialRatio = this.healthRatio(HealthType.init); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const initialHealth = this.health(HealthType.init); if (initialRatio.lte(ZERO_I80F48())) { return ZERO_I80F48(); @@ -481,7 +519,7 @@ export class HealthCache { // negative. // The maximum will be at one of these points (ignoring serum3 effects). - function cacheAfterSwap(amount: I80F48) { + function cacheAfterSwap(amount: I80F48): HealthCache { const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); // HealthCache.logHealthCache('beforeSwap', adjustedCache); adjustedCache.tokenInfos[sourceIndex].balance.isub(amount); @@ -578,24 +616,12 @@ export class HealthCache { } getMaxSerum3OrderForHealthRatio( - group: Group, + baseBank: BankForHealth, + quoteBank: BankForHealth, serum3Market: Serum3Market, side: Serum3Side, minRatio: I80F48, - ) { - const baseBank = group.getFirstBankByTokenIndex( - serum3Market.baseTokenIndex, - ); - if (!baseBank) { - throw new Error(`No bank for index ${serum3Market.baseTokenIndex}`); - } - const quoteBank = group.getFirstBankByTokenIndex( - serum3Market.quoteTokenIndex, - ); - if (!quoteBank) { - throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`); - } - + ): I80F48 { const healthCacheClone: HealthCache = _.cloneDeep(this); const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank); @@ -652,10 +678,11 @@ export class HealthCache { } const cache = cacheAfterPlacingOrder(zeroAmount); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const zeroAmountHealth = cache.health(HealthType.init); const zeroAmountRatio = cache.healthRatio(HealthType.init); - function cacheAfterPlacingOrder(amount: I80F48) { + function cacheAfterPlacingOrder(amount: I80F48): HealthCache { const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); side === Serum3Side.ask @@ -663,7 +690,8 @@ export class HealthCache { : adjustedCache.tokenInfos[quoteIndex].balance.isub(amount); adjustedCache.adjustSerum3Reserved( - group, + baseBank, + quoteBank, serum3Market, side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48(), ZERO_I80F48(), @@ -687,18 +715,7 @@ export class HealthCache { healthRatioAfterPlacingOrder, ); - // If its a bid then the reserved fund and potential loan is in quote, - // If its a ask then the reserved fund and potential loan is in base, - // also keep some buffer for fees, use taker fees for worst case simulation. - return side === Serum3Side.bid - ? amount - .div(quote.oraclePrice) - .div(ONE_I80F48().add(baseBank.loanOriginationFeeRate)) - .div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false)))) - : amount - .div(base.oraclePrice) - .div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate)) - .div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false)))); + return amount; } getMaxPerpForHealthRatio( @@ -813,7 +830,7 @@ export class HealthCache { export class TokenInfo { constructor( - public tokenIndex: number, + public tokenIndex: TokenIndex, public maintAssetWeight: I80F48, public initAssetWeight: I80F48, public maintLiabWeight: I80F48, @@ -828,7 +845,7 @@ export class TokenInfo { static fromDto(dto: TokenInfoDto): TokenInfo { return new TokenInfo( - dto.tokenIndex, + dto.tokenIndex as TokenIndex, I80F48.from(dto.maintAssetWeight), I80F48.from(dto.initAssetWeight), I80F48.from(dto.maintLiabWeight), @@ -839,11 +856,11 @@ export class TokenInfo { ); } - static emptyFromBank(bank: BankForHealth): TokenInfo { - if (!bank.price) - throw new Error( - `Failed to create TokenInfo. Bank price unavailable for bank with tokenIndex ${bank.tokenIndex}`, - ); + static fromBank( + bank: BankForHealth, + nativeBalance?: I80F48, + serum3MaxReserved?: I80F48, + ): TokenInfo { return new TokenInfo( bank.tokenIndex, bank.maintAssetWeight, @@ -851,8 +868,8 @@ export class TokenInfo { bank.maintLiabWeight, bank.initLiabWeight, bank.price, - ZERO_I80F48(), - ZERO_I80F48(), + nativeBalance ? nativeBalance.mul(bank.price) : ZERO_I80F48(), + serum3MaxReserved ? serum3MaxReserved : ZERO_I80F48(), ); } @@ -876,7 +893,7 @@ export class TokenInfo { ).mul(this.balance); } - toString() { + toString(): string { return ` tokenIndex: ${this.tokenIndex}, balance: ${ this.balance }, serum3MaxReserved: ${ @@ -890,15 +907,15 @@ export class Serum3Info { public reserved: I80F48, public baseIndex: number, public quoteIndex: number, - public marketIndex: number, + public marketIndex: MarketIndex, ) {} - static fromDto(dto: Serum3InfoDto) { + static fromDto(dto: Serum3InfoDto): Serum3Info { return new Serum3Info( I80F48.from(dto.reserved), dto.baseIndex, dto.quoteIndex, - dto.marketIndex, + dto.marketIndex as MarketIndex, ); } @@ -906,7 +923,7 @@ export class Serum3Info { serum3Market: Serum3Market, baseEntryIndex: number, quoteEntryIndex: number, - ) { + ): Serum3Info { return new Serum3Info( ZERO_I80F48(), baseEntryIndex, @@ -915,10 +932,47 @@ export class Serum3Info { ); } + static fromOoModifyingTokenInfos( + baseIndex: number, + baseInfo: TokenInfo, + quoteIndex: number, + quoteInfo: TokenInfo, + marketIndex: MarketIndex, + oo: OpenOrders, + ): Serum3Info { + // add the amounts that are freely settleable + const baseFree = I80F48.fromString(oo.baseTokenFree.toString()); + // NOTE: referrerRebatesAccrued is not declared on oo class, but the layout + // is aware of it + const quoteFree = I80F48.fromString( + oo.quoteTokenFree.add((oo as any).referrerRebatesAccrued).toString(), + ); + baseInfo.balance.iadd(baseFree.mul(baseInfo.oraclePrice)); + quoteInfo.balance.iadd(quoteFree.mul(quoteInfo.oraclePrice)); + + // add the reserved amount to both sides, to have the worst-case covered + const reservedBase = I80F48.fromString( + oo.baseTokenTotal.sub(oo.baseTokenFree).toString(), + ); + const reservedQuote = I80F48.fromString( + oo.quoteTokenTotal.sub(oo.quoteTokenFree).toString(), + ); + const reservedBalance = reservedBase + .mul(baseInfo.oraclePrice) + .add(reservedQuote.mul(quoteInfo.oraclePrice)); + baseInfo.serum3MaxReserved.iadd(reservedBalance); + quoteInfo.serum3MaxReserved.iadd(reservedBalance); + + return new Serum3Info(reservedBalance, baseIndex, quoteIndex, marketIndex); + } + healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 { const baseInfo = tokenInfos[this.baseIndex]; const quoteInfo = tokenInfos[this.quoteIndex]; const reserved = this.reserved; + // console.log(` - reserved ${reserved}`); + // console.log(` - this.baseIndex ${this.baseIndex}`); + // console.log(` - this.quoteIndex ${this.quoteIndex}`); if (reserved.isZero()) { return ZERO_I80F48(); @@ -926,7 +980,7 @@ export class Serum3Info { // How much the health would increase if the reserved balance were applied to the passed // token info? - const computeHealthEffect = function (tokenInfo: TokenInfo) { + const computeHealthEffect = function (tokenInfo: TokenInfo): I80F48 { // This balance includes all possible reserved funds from markets that relate to the // token, including this market itself: `reserved` is already included in `max_balance`. const maxBalance = tokenInfo.balance.add(tokenInfo.serum3MaxReserved); @@ -946,15 +1000,25 @@ export class Serum3Info { } const assetWeight = tokenInfo.assetWeight(healthType); const liabWeight = tokenInfo.liabWeight(healthType); + + // console.log(` - tokenInfo.index ${tokenInfo.tokenIndex}`); + // console.log(` - tokenInfo.balance ${tokenInfo.balance}`); + // console.log( + // ` - tokenInfo.serum3MaxReserved ${tokenInfo.serum3MaxReserved}`, + // ); + // console.log(` - assetPart ${assetPart}`); + // console.log(` - liabPart ${liabPart}`); return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart)); }; const reservedAsBase = computeHealthEffect(baseInfo); const reservedAsQuote = computeHealthEffect(quoteInfo); + // console.log(` - reservedAsBase ${reservedAsBase}`); + // console.log(` - reservedAsQuote ${reservedAsQuote}`); return reservedAsBase.min(reservedAsQuote); } - toString(tokenInfos: TokenInfo[]) { + toString(tokenInfos: TokenInfo[]): string { return ` marketIndex: ${this.marketIndex}, baseIndex: ${ this.baseIndex }, quoteIndex: ${this.quoteIndex}, reserved: ${ @@ -970,15 +1034,14 @@ export class PerpInfo { public initAssetWeight: I80F48, public maintLiabWeight: I80F48, public initLiabWeight: I80F48, - // in health-reference-token native units, needs scaling by asset/liab public base: I80F48, - // in health-reference-token native units, no asset/liab factor needed public quote: I80F48, public oraclePrice: I80F48, public hasOpenOrders: boolean, + public trustedMarket: boolean, ) {} - static fromDto(dto: PerpInfoDto) { + static fromDto(dto: PerpInfoDto): PerpInfo { return new PerpInfo( dto.perpMarketIndex, I80F48.from(dto.maintAssetWeight), @@ -989,6 +1052,114 @@ export class PerpInfo { I80F48.from(dto.quote), I80F48.from(dto.oraclePrice), dto.hasOpenOrders, + dto.trustedMarket, + ); + } + + static fromPerpPosition( + perpMarket: PerpMarket, + perpPosition: PerpPosition, + ): PerpInfo { + const baseLotSize = I80F48.fromString(perpMarket.baseLotSize.toString()); + const baseLots = I80F48.fromNumber( + perpPosition.basePositionLots + perpPosition.takerBaseLots, + ); + + const unsettledFunding = perpPosition.unsettledFunding(perpMarket); + + const takerQuote = I80F48.fromString( + new BN(perpPosition.takerQuoteLots) + .mul(perpMarket.quoteLotSize) + .toString(), + ); + const quoteCurrent = I80F48.fromString( + perpPosition.quotePositionNative.toString(), + ) + .sub(unsettledFunding) + .add(takerQuote); + + // Two scenarios: + // 1. The price goes low and all bids execute, converting to base. + // That means the perp position is increased by `bids` and the quote position + // is decreased by `bids * baseLotSize * price`. + // The health for this case is: + // (weighted(baseLots + bids) - bids) * baseLotSize * price + quote + // 2. The price goes high and all asks execute, converting to quote. + // The health for this case is: + // (weighted(baseLots - asks) + asks) * baseLotSize * price + quote + // + // Comparing these makes it clear we need to pick the worse subfactor + // weighted(baseLots + bids) - bids =: scenario1 + // or + // weighted(baseLots - asks) + asks =: scenario2 + // + // Additionally, we want this scenario choice to be the same no matter whether we're + // computing init or maint health. This can be guaranteed by requiring the weights + // to satisfy the property (P): + // + // (1 - initAssetWeight) / (initLiabWeight - 1) + // == (1 - maintAssetWeight) / (maintLiabWeight - 1) + // + // Derivation: + // Set asksNetLots := baseLots - asks, bidsNetLots := baseLots + bids. + // Now + // scenario1 = weighted(bidsNetLots) - bidsNetLots + baseLots and + // scenario2 = weighted(asksNetLots) - asksNetLots + baseLots + // So with expanding weigthed(a) = weightFactorForA * a, the question + // scenario1 < scenario2 + // becomes: + // (weightFactorForBidsNetLots - 1) * bidsNetLots + // < (weightFactorForAsksNetLots - 1) * asksNetLots + // Since asksNetLots < 0 and bidsNetLots > 0 is the only interesting case, (P) follows. + // + // We satisfy (P) by requiring + // assetWeight = 1 - x and liabWeight = 1 + x + // + // And with that assumption the scenario choice condition further simplifies to: + // scenario1 < scenario2 + // iff abs(bidsNetLots) > abs(asksNetLots) + + const bidsNetLots = baseLots.add( + I80F48.fromNumber(perpPosition.bidsBaseLots), + ); + const asksNetLots = baseLots.sub( + I80F48.fromNumber(perpPosition.asksBaseLots), + ); + + const lotsToQuote = baseLotSize.mul(perpMarket.price); + + let base, quote; + if (bidsNetLots.abs().gt(asksNetLots.abs())) { + const bidsBaseLots = I80F48.fromString( + perpPosition.bidsBaseLots.toString(), + ); + base = bidsNetLots.mul(lotsToQuote); + quote = quoteCurrent.sub(bidsBaseLots.mul(lotsToQuote)); + } else { + const asksBaseLots = I80F48.fromString( + perpPosition.asksBaseLots.toString(), + ); + base = asksNetLots.mul(lotsToQuote); + quote = quoteCurrent.add(asksBaseLots.mul(lotsToQuote)); + } + + // console.log(`bidsNetLots ${bidsNetLots}`); + // console.log(`asksNetLots ${asksNetLots}`); + // console.log(`quoteCurrent ${quoteCurrent}`); + // console.log(`base ${base}`); + // console.log(`quote ${quote}`); + + return new PerpInfo( + perpMarket.perpMarketIndex, + perpMarket.maintAssetWeight, + perpMarket.initAssetWeight, + perpMarket.maintLiabWeight, + perpMarket.initLiabWeight, + base, + quote, + perpMarket.price, + perpPosition.hasOpenOrders(), + perpMarket.trustedMarket, ); } @@ -1006,16 +1177,21 @@ export class PerpInfo { weight = this.maintAssetWeight; } - // FUTURE: Allow v3-style "reliable" markets where we can return - // `self.quote + weight * self.base` here - return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48()); + // console.log(`initLiabWeight ${this.initLiabWeight}`); + // console.log(`initAssetWeight ${this.initAssetWeight}`); + // console.log(`weight ${weight}`); + // console.log(`this.quote ${this.quote}`); + // console.log(`this.base ${this.base}`); + + const uncappedHealthContribution = this.quote.add(weight.mul(this.base)); + if (this.trustedMarket) { + return uncappedHealthContribution; + } else { + return uncappedHealthContribution.min(ZERO_I80F48()); + } } static emptyFromPerpMarket(perpMarket: PerpMarket): PerpInfo { - if (!perpMarket.price) - throw new Error( - `Failed to create PerpInfo. Oracle price unavailable. ${perpMarket.oracle.toString()}`, - ); return new PerpInfo( perpMarket.perpMarketIndex, perpMarket.maintAssetWeight, @@ -1024,10 +1200,19 @@ export class PerpInfo { perpMarket.initLiabWeight, ZERO_I80F48(), ZERO_I80F48(), - I80F48.fromNumber(perpMarket.price), + perpMarket.price, false, + perpMarket.trustedMarket, ); } + + toString(): string { + return ` perpMarketIndex: ${this.perpMarketIndex}, base: ${ + this.base + }, quote: ${this.quote}, oraclePrice: ${ + this.oraclePrice + }, initHealth ${this.healthContribution(HealthType.init)}`; + } } export class HealthCacheDto { @@ -1087,10 +1272,9 @@ export class PerpInfoDto { initAssetWeight: I80F48Dto; maintLiabWeight: I80F48Dto; initLiabWeight: I80F48Dto; - // in health-reference-token native units, needs scaling by asset/liab base: I80F48Dto; - // in health-reference-token native units, no asset/liab factor needed quote: I80F48Dto; oraclePrice: I80F48Dto; hasOpenOrders: boolean; + trustedMarket: boolean; } diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index d97166bb5..1bc777101 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -1,20 +1,21 @@ import { BN } from '@project-serum/anchor'; import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; -import { Order, Orderbook } from '@project-serum/serum/lib/market'; +import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market'; import { PublicKey } from '@solana/web3.js'; import { MangoClient } from '../client'; +import { SERUM3_PROGRAM_ID } from '../constants'; import { nativeI80F48ToUi, toNative, toUiDecimals, toUiDecimalsForQuote, } from '../utils'; -import { Bank } from './bank'; +import { Bank, TokenIndex } from './bank'; import { Group } from './group'; -import { HealthCache, HealthCacheDto } from './healthCache'; +import { HealthCache } from './healthCache'; import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48'; -import { PerpOrder, PerpOrderSide } from './perp'; -import { Serum3Side } from './serum3'; +import { PerpMarket, PerpMarketIndex, PerpOrder, PerpOrderSide } from './perp'; +import { MarketIndex, Serum3Side } from './serum3'; export class MangoAccount { public tokens: TokenPosition[]; public serum3: Serum3Orders[]; @@ -58,7 +59,7 @@ export class MangoAccount { obj.serum3 as Serum3PositionDto[], obj.perps as PerpPositionDto[], obj.perpOpenOrders as PerpOoDto[], - {} as any, + new Map(), // serum3OosMapByMarketIndex ); } @@ -78,39 +79,56 @@ export class MangoAccount { serum3: Serum3PositionDto[], perps: PerpPositionDto[], perpOpenOrders: PerpOoDto[], - public accountData: undefined | MangoAccountData, + public serum3OosMapByMarketIndex: Map, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.tokens = tokens.map((dto) => TokenPosition.from(dto)); this.serum3 = serum3.map((dto) => Serum3Orders.from(dto)); this.perps = perps.map((dto) => PerpPosition.from(dto)); this.perpOpenOrders = perpOpenOrders.map((dto) => PerpOo.from(dto)); - this.accountData = undefined; this.netDeposits = netDeposits; } - async reload(client: MangoClient, group: Group): Promise { + async reload(client: MangoClient): Promise { const mangoAccount = await client.getMangoAccount(this); - await mangoAccount.reloadAccountData(client, group); + await mangoAccount.reloadAccountData(client); Object.assign(this, mangoAccount); return mangoAccount; } async reloadWithSlot( client: MangoClient, - group: Group, ): Promise<{ value: MangoAccount; slot: number }> { const resp = await client.getMangoAccountWithSlot(this.publicKey); - await resp?.value.reloadAccountData(client, group); + await resp?.value.reloadAccountData(client); Object.assign(this, resp?.value); return { value: resp!.value, slot: resp!.slot }; } - async reloadAccountData( - client: MangoClient, - group: Group, - ): Promise { - this.accountData = await client.computeAccountData(group, this); + async reloadAccountData(client: MangoClient): Promise { + const serum3Active = this.serum3Active(); + const ais = + await client.program.provider.connection.getMultipleAccountsInfo( + serum3Active.map((serum3) => serum3.openOrders), + ); + this.serum3OosMapByMarketIndex = new Map( + Array.from( + ais.map((ai, i) => { + if (!ai) { + throw new Error( + `Undefined AI for open orders ${serum3Active[i].openOrders} and market ${serum3Active[i].marketIndex}!`, + ); + } + const oo = OpenOrders.fromAccountInfo( + serum3Active[i].openOrders, + ai, + SERUM3_PROGRAM_ID[client.cluster], + ); + return [serum3Active[i].marketIndex, oo]; + }), + ), + ); + return this; } @@ -132,14 +150,26 @@ export class MangoAccount { ); } - findToken(tokenIndex: number): TokenPosition | undefined { + getToken(tokenIndex: TokenIndex): TokenPosition | undefined { return this.tokens.find((ta) => ta.tokenIndex == tokenIndex); } - findSerum3Account(marketIndex: number): Serum3Orders | undefined { + getSerum3Account(marketIndex: MarketIndex): Serum3Orders | undefined { return this.serum3.find((sa) => sa.marketIndex == marketIndex); } + getSerum3OoAccount(marketIndex: MarketIndex): OpenOrders { + const oo: OpenOrders | undefined = + this.serum3OosMapByMarketIndex.get(marketIndex); + + if (!oo) { + throw new Error( + `Open orders account not loaded for market with marketIndex ${marketIndex}!`, + ); + } + return oo; + } + // How to navigate // * if a function is returning a I80F48, then usually the return value is in native quote or native token, unless specified // * if a function is returning a number, then usually the return value is in ui tokens, unless specified @@ -152,7 +182,7 @@ export class MangoAccount { * @returns native balance for a token, is signed */ getTokenBalance(bank: Bank): I80F48 { - const tp = this.findToken(bank.tokenIndex); + const tp = this.getToken(bank.tokenIndex); return tp ? tp.balance(bank) : ZERO_I80F48(); } @@ -162,7 +192,7 @@ export class MangoAccount { * @returns native deposits for a token, 0 if position has borrows */ getTokenDeposits(bank: Bank): I80F48 { - const tp = this.findToken(bank.tokenIndex); + const tp = this.getToken(bank.tokenIndex); return tp ? tp.deposits(bank) : ZERO_I80F48(); } @@ -172,7 +202,7 @@ export class MangoAccount { * @returns native borrows for a token, 0 if position has deposits */ getTokenBorrows(bank: Bank): I80F48 { - const tp = this.findToken(bank.tokenIndex); + const tp = this.getToken(bank.tokenIndex); return tp ? tp.borrows(bank) : ZERO_I80F48(); } @@ -182,7 +212,7 @@ export class MangoAccount { * @returns UI balance for a token, is signed */ getTokenBalanceUi(bank: Bank): number { - const tp = this.findToken(bank.tokenIndex); + const tp = this.getToken(bank.tokenIndex); return tp ? tp.balanceUi(bank) : 0; } @@ -192,7 +222,7 @@ export class MangoAccount { * @returns UI deposits for a token, 0 or more */ getTokenDepositsUi(bank: Bank): number { - const ta = this.findToken(bank.tokenIndex); + const ta = this.getToken(bank.tokenIndex); return ta ? ta.depositsUi(bank) : 0; } @@ -202,7 +232,7 @@ export class MangoAccount { * @returns UI borrows for a token, 0 or less */ getTokenBorrowsUi(bank: Bank): number { - const ta = this.findToken(bank.tokenIndex); + const ta = this.getToken(bank.tokenIndex); return ta ? ta.borrowsUi(bank) : 0; } @@ -211,10 +241,9 @@ export class MangoAccount { * @param healthType * @returns raw health number, in native quote */ - getHealth(healthType: HealthType): I80F48 | undefined { - return healthType == HealthType.init - ? this.accountData?.initHealth - : this.accountData?.maintHealth; + getHealth(group: Group, healthType: HealthType): I80F48 { + const hc = HealthCache.fromMangoAccount(group, this); + return hc.health(healthType); } /** @@ -223,8 +252,9 @@ export class MangoAccount { * @param healthType * @returns health ratio, in percentage form */ - getHealthRatio(healthType: HealthType): I80F48 | undefined { - return this.accountData?.healthCache.healthRatio(healthType); + getHealthRatio(group: Group, healthType: HealthType): I80F48 { + const hc = HealthCache.fromMangoAccount(group, this); + return hc.healthRatio(healthType); } /** @@ -232,8 +262,8 @@ export class MangoAccount { * @param healthType * @returns health ratio, in percentage form, capped to 100 */ - getHealthRatioUi(healthType: HealthType): number | undefined { - const ratio = this.getHealthRatio(healthType)?.toNumber(); + getHealthRatioUi(group: Group, healthType: HealthType): number | undefined { + const ratio = this.getHealthRatio(group, healthType).toNumber(); if (ratio) { return ratio > 100 ? 100 : Math.trunc(ratio); } else { @@ -245,40 +275,73 @@ export class MangoAccount { * Sum of all the assets i.e. token deposits, borrows, total assets in spot open orders, (perps positions is todo) in terms of quote value. * @returns equity, in native quote */ - getEquity(): I80F48 | undefined { - if (this.accountData) { - const equity = this.accountData.equity; - const total_equity = equity.tokens.reduce( - (a, b) => a.add(b.value), - ZERO_I80F48(), - ); - return total_equity; + getEquity(group: Group): I80F48 { + const tokensMap = new Map(); + for (const tp of this.tokensActive()) { + const bank = group.getFirstBankByTokenIndex(tp.tokenIndex); + tokensMap.set(tp.tokenIndex, tp.balance(bank).mul(bank.price)); } - return undefined; + + for (const sp of this.serum3Active()) { + const oo = this.getSerum3OoAccount(sp.marketIndex); + const baseBank = group.getFirstBankByTokenIndex(sp.baseTokenIndex); + tokensMap + .get(baseBank.tokenIndex)! + .iadd( + I80F48.fromString(oo.baseTokenTotal.toString()).mul(baseBank.price), + ); + const quoteBank = group.getFirstBankByTokenIndex(sp.quoteTokenIndex); + // NOTE: referrerRebatesAccrued is not declared on oo class, but the layout + // is aware of it + tokensMap + .get(baseBank.tokenIndex)! + .iadd( + I80F48.fromString( + oo.quoteTokenTotal + .add((oo as any).referrerRebatesAccrued) + .toString(), + ).mul(quoteBank.price), + ); + } + + const tokenEquity = Array.from(tokensMap.values()).reduce( + (a, b) => a.add(b), + ZERO_I80F48(), + ); + + const perpEquity = this.perpActive().reduce( + (a, b) => + a.add(b.getEquity(group.getPerpMarketByMarketIndex(b.marketIndex))), + ZERO_I80F48(), + ); + + return tokenEquity.add(perpEquity); } /** * The amount of native quote you could withdraw against your existing assets. * @returns collateral value, in native quote */ - getCollateralValue(): I80F48 | undefined { - return this.getHealth(HealthType.init); + getCollateralValue(group: Group): I80F48 { + return this.getHealth(group, HealthType.init); } /** * Sum of all positive assets. * @returns assets, in native quote */ - getAssetsValue(healthType: HealthType): I80F48 | undefined { - return this.accountData?.healthCache.assets(healthType); + getAssetsValue(group: Group, healthType: HealthType): I80F48 { + const hc = HealthCache.fromMangoAccount(group, this); + return hc.assets(healthType); } /** * Sum of all negative assets. * @returns liabs, in native quote */ - getLiabsValue(healthType: HealthType): I80F48 | undefined { - return this.accountData?.healthCache.liabs(healthType); + getLiabsValue(group: Group, healthType: HealthType): I80F48 { + const hc = HealthCache.fromMangoAccount(group, this); + return hc.liabs(healthType); } /** @@ -286,8 +349,8 @@ export class MangoAccount { * PNL is defined here as spot value + serum3 open orders value + perp value - net deposits value (evaluated at native quote price at the time of the deposit/withdraw) * spot value + serum3 open orders value + perp value is returned by getEquity (open orders values are added to spot token values implicitly) */ - getPnl(): I80F48 | undefined { - return this.getEquity()?.add( + getPnl(group: Group): I80F48 { + return this.getEquity(group)?.add( I80F48.fromI64(this.netDeposits).mul(I80F48.fromNumber(-1)), ); } @@ -301,7 +364,7 @@ export class MangoAccount { mintPk: PublicKey, ): I80F48 | undefined { const tokenBank: Bank = group.getFirstBankByMint(mintPk); - const initHealth = this.accountData?.initHealth; + const initHealth = this.getHealth(group, HealthType.init); if (!initHealth) return undefined; @@ -314,8 +377,7 @@ export class MangoAccount { // Deposits need special treatment since they would neither count towards liabilities // nor would be charged loanOriginationFeeRate when withdrawn - const tp = this.findToken(tokenBank.tokenIndex); - if (!tokenBank.price) return undefined; + const tp = this.getToken(tokenBank.tokenIndex); const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48(); let existingPositionHealthContrib = ZERO_I80F48(); if (existingTokenDeposits.gt(ZERO_I80F48())) { @@ -377,18 +439,14 @@ export class MangoAccount { targetMintPk: PublicKey, priceFactor: number, ): number | undefined { - if (!this.accountData) { - throw new Error( - `accountData not loaded on MangoAccount, try reloading MangoAccount`, - ); - } if (sourceMintPk.equals(targetMintPk)) { return 0; } - const maxSource = this.accountData.healthCache.getMaxSourceForTokenSwap( + const hc = HealthCache.fromMangoAccount(group, this); + const maxSource = hc.getMaxSourceForTokenSwap( group.getFirstBankByMint(sourceMintPk), group.getFirstBankByMint(targetMintPk), - ONE_I80F48(), // target 1% health + I80F48.fromNumber(2), // target 2% health I80F48.fromNumber(priceFactor), ); maxSource.idiv( @@ -426,7 +484,8 @@ export class MangoAccount { mintPk: tokenChange.mintPk, }; }); - return this.accountData?.healthCache + const hc = HealthCache.fromMangoAccount(group, this); + return hc .simHealthRatioWithTokenPositionChanges( group, nativeTokenChanges, @@ -440,14 +499,8 @@ export class MangoAccount { group: Group, externalMarketPk: PublicKey, ): Promise { - const serum3Market = group.serum3MarketsMapByExternal.get( - externalMarketPk.toBase58(), - ); - if (!serum3Market) { - throw new Error( - `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, - ); - } + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); const serum3OO = this.serum3Active().find( (s) => s.marketIndex === serum3Market.marketIndex, ); @@ -455,7 +508,7 @@ export class MangoAccount { throw new Error(`No open orders account found for ${externalMarketPk}`); } - const serum3MarketExternal = group.serum3MarketExternalsMap.get( + const serum3MarketExternal = group.serum3ExternalMarketsMap.get( externalMarketPk.toBase58(), )!; const [bidsInfo, asksInfo] = @@ -463,9 +516,14 @@ export class MangoAccount { serum3MarketExternal.bidsAddress, serum3MarketExternal.asksAddress, ]); - if (!bidsInfo || !asksInfo) { + if (!bidsInfo) { throw new Error( - `bids and asks ai were not fetched for ${externalMarketPk.toString()}`, + `Undefined bidsInfo for serum3Market with externalMarket ${externalMarketPk.toString()!}`, + ); + } + if (!asksInfo) { + throw new Error( + `Undefined asksInfo for serum3Market with externalMarket ${externalMarketPk.toString()!}`, ); } const bids = Orderbook.decode(serum3MarketExternal, bidsInfo.data); @@ -476,7 +534,6 @@ export class MangoAccount { } /** - * TODO priceFactor * @param group * @param externalMarketPk * @returns maximum ui quote which can be traded for base token given current health @@ -485,26 +542,28 @@ export class MangoAccount { group: Group, externalMarketPk: PublicKey, ): number { - const serum3Market = group.serum3MarketsMapByExternal.get( - externalMarketPk.toBase58(), + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); + const baseBank = group.getFirstBankByTokenIndex( + serum3Market.baseTokenIndex, ); - if (!serum3Market) { - throw new Error( - `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, - ); - } - if (!this.accountData) { - throw new Error( - `accountData not loaded on MangoAccount, try reloading MangoAccount`, - ); - } - const nativeAmount = - this.accountData.healthCache.getMaxSerum3OrderForHealthRatio( - group, - serum3Market, - Serum3Side.bid, - I80F48.fromNumber(1), - ); + const quoteBank = group.getFirstBankByTokenIndex( + serum3Market.quoteTokenIndex, + ); + const hc = HealthCache.fromMangoAccount(group, this); + let nativeAmount = hc.getMaxSerum3OrderForHealthRatio( + baseBank, + quoteBank, + serum3Market, + Serum3Side.bid, + I80F48.fromNumber(2), + ); + // If its a bid then the reserved fund and potential loan is in base + // also keep some buffer for fees, use taker fees for worst case simulation. + nativeAmount = nativeAmount + .div(quoteBank.price) + .div(ONE_I80F48().add(baseBank.loanOriginationFeeRate)) + .div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false)))); return toUiDecimals( nativeAmount, group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals, @@ -512,7 +571,6 @@ export class MangoAccount { } /** - * TODO priceFactor * @param group * @param externalMarketPk * @returns maximum ui base which can be traded for quote token given current health @@ -521,26 +579,28 @@ export class MangoAccount { group: Group, externalMarketPk: PublicKey, ): number { - const serum3Market = group.serum3MarketsMapByExternal.get( - externalMarketPk.toBase58(), + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); + const baseBank = group.getFirstBankByTokenIndex( + serum3Market.baseTokenIndex, ); - if (!serum3Market) { - throw new Error( - `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, - ); - } - if (!this.accountData) { - throw new Error( - `accountData not loaded on MangoAccount, try reloading MangoAccount`, - ); - } - const nativeAmount = - this.accountData.healthCache.getMaxSerum3OrderForHealthRatio( - group, - serum3Market, - Serum3Side.ask, - I80F48.fromNumber(1), - ); + const quoteBank = group.getFirstBankByTokenIndex( + serum3Market.quoteTokenIndex, + ); + const hc = HealthCache.fromMangoAccount(group, this); + let nativeAmount = hc.getMaxSerum3OrderForHealthRatio( + baseBank, + quoteBank, + serum3Market, + Serum3Side.ask, + I80F48.fromNumber(2), + ); + // If its a ask then the reserved fund and potential loan is in base + // also keep some buffer for fees, use taker fees for worst case simulation. + nativeAmount = nativeAmount + .div(baseBank.price) + .div(ONE_I80F48().add(baseBank.loanOriginationFeeRate)) + .div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false)))); return toUiDecimals( nativeAmount, group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals, @@ -561,22 +621,19 @@ export class MangoAccount { externalMarketPk: PublicKey, healthType: HealthType = HealthType.init, ): number { - const serum3Market = group.serum3MarketsMapByExternal.get( - externalMarketPk.toBase58(), + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); + const baseBank = group.getFirstBankByTokenIndex( + serum3Market.baseTokenIndex, ); - if (!serum3Market) { - throw new Error( - `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, - ); - } - if (!this.accountData) { - throw new Error( - `accountData not loaded on MangoAccount, try reloading MangoAccount`, - ); - } - return this.accountData.healthCache + const quoteBank = group.getFirstBankByTokenIndex( + serum3Market.quoteTokenIndex, + ); + const hc = HealthCache.fromMangoAccount(group, this); + return hc .simHealthRatioWithSerum3BidChanges( - group, + baseBank, + quoteBank, toNative( uiQuoteAmount, group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) @@ -602,22 +659,19 @@ export class MangoAccount { externalMarketPk: PublicKey, healthType: HealthType = HealthType.init, ): number { - const serum3Market = group.serum3MarketsMapByExternal.get( - externalMarketPk.toBase58(), + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); + const baseBank = group.getFirstBankByTokenIndex( + serum3Market.baseTokenIndex, ); - if (!serum3Market) { - throw new Error( - `Unable to find mint serum3Market for ${externalMarketPk.toString()}`, - ); - } - if (!this.accountData) { - throw new Error( - `accountData not loaded on MangoAccount, try reloading MangoAccount`, - ); - } - return this.accountData.healthCache + const quoteBank = group.getFirstBankByTokenIndex( + serum3Market.quoteTokenIndex, + ); + const hc = HealthCache.fromMangoAccount(group, this); + return hc .simHealthRatioWithSerum3AskChanges( - group, + baseBank, + quoteBank, toNative( uiBaseAmount, group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) @@ -638,28 +692,21 @@ export class MangoAccount { */ public getMaxQuoteForPerpBidUi( group: Group, - perpMarketName: string, + perpMarketIndex: PerpMarketIndex, uiPrice: number, ): number { - const perpMarket = group.perpMarketsMap.get(perpMarketName); - if (!perpMarket) { - throw new Error(`PerpMarket for ${perpMarketName} not found!`); - } - if (!this.accountData) { - throw new Error( - `accountData not loaded on MangoAccount, try reloading MangoAccount`, - ); - } - const baseLots = this.accountData.healthCache.getMaxPerpForHealthRatio( + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); + const hc = HealthCache.fromMangoAccount(group, this); + const baseLots = hc.getMaxPerpForHealthRatio( perpMarket, PerpOrderSide.bid, - I80F48.fromNumber(1), + I80F48.fromNumber(2), group.toNativePrice(uiPrice, perpMarket.baseDecimals), ); const nativeBase = baseLots.mul( I80F48.fromString(perpMarket.baseLotSize.toString()), ); - const nativeQuote = nativeBase.mul(I80F48.fromNumber(perpMarket.price)); + const nativeQuote = nativeBase.mul(perpMarket.price); return toUiDecimalsForQuote(nativeQuote.toNumber()); } @@ -672,22 +719,15 @@ export class MangoAccount { */ public getMaxBaseForPerpAskUi( group: Group, - perpMarketName: string, + perpMarketIndex: PerpMarketIndex, uiPrice: number, ): number { - const perpMarket = group.perpMarketsMap.get(perpMarketName); - if (!perpMarket) { - throw new Error(`PerpMarket for ${perpMarketName} not found!`); - } - if (!this.accountData) { - throw new Error( - `accountData not loaded on MangoAccount, try reloading MangoAccount`, - ); - } - const baseLots = this.accountData.healthCache.getMaxPerpForHealthRatio( + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); + const hc = HealthCache.fromMangoAccount(group, this); + const baseLots = hc.getMaxPerpForHealthRatio( perpMarket, PerpOrderSide.ask, - I80F48.fromNumber(1), + I80F48.fromNumber(2), group.toNativePrice(uiPrice, perpMarket.baseDecimals), ); return perpMarket.baseLotsToUi(new BN(baseLots.toString())); @@ -696,12 +736,9 @@ export class MangoAccount { public async loadPerpOpenOrdersForMarket( client: MangoClient, group: Group, - perpMarketName: string, + perpMarketIndex: PerpMarketIndex, ): Promise { - const perpMarket = group.perpMarketsMap.get(perpMarketName); - if (!perpMarket) { - throw new Error(`Perp Market ${perpMarketName} not found!`); - } + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const [bids, asks] = await Promise.all([ perpMarket.loadBids(client), perpMarket.loadAsks(client), @@ -759,17 +796,17 @@ export class MangoAccount { export class TokenPosition { static TokenIndexUnset = 65535; - static from(dto: TokenPositionDto) { + static from(dto: TokenPositionDto): TokenPosition { return new TokenPosition( I80F48.from(dto.indexedPosition), - dto.tokenIndex, + dto.tokenIndex as TokenIndex, dto.inUseCount, ); } constructor( public indexedPosition: I80F48, - public tokenIndex: number, + public tokenIndex: TokenIndex, public inUseCount: number, ) {} @@ -877,17 +914,17 @@ export class Serum3Orders { static from(dto: Serum3PositionDto): Serum3Orders { return new Serum3Orders( dto.openOrders, - dto.marketIndex, - dto.baseTokenIndex, - dto.quoteTokenIndex, + dto.marketIndex as MarketIndex, + dto.baseTokenIndex as TokenIndex, + dto.quoteTokenIndex as TokenIndex, ); } constructor( public openOrders: PublicKey, - public marketIndex: number, - public baseTokenIndex: number, - public quoteTokenIndex: number, + public marketIndex: MarketIndex, + public baseTokenIndex: TokenIndex, + public quoteTokenIndex: TokenIndex, ) {} public isActive(): boolean { @@ -907,31 +944,77 @@ export class Serum3PositionDto { export class PerpPosition { static PerpMarketIndexUnset = 65535; - static from(dto: PerpPositionDto) { + static from(dto: PerpPositionDto): PerpPosition { return new PerpPosition( - dto.marketIndex, + dto.marketIndex as PerpMarketIndex, dto.basePositionLots.toNumber(), - dto.quotePositionNative.val, + I80F48.from(dto.quotePositionNative), dto.bidsBaseLots.toNumber(), dto.asksBaseLots.toNumber(), dto.takerBaseLots.toNumber(), dto.takerQuoteLots.toNumber(), + I80F48.from(dto.longSettledFunding), + I80F48.from(dto.shortSettledFunding), ); } constructor( - public marketIndex: number, + public marketIndex: PerpMarketIndex, public basePositionLots: number, - public quotePositionNative: BN, + public quotePositionNative: I80F48, public bidsBaseLots: number, public asksBaseLots: number, public takerBaseLots: number, public takerQuoteLots: number, + public longSettledFunding: I80F48, + public shortSettledFunding: I80F48, ) {} isActive(): boolean { return this.marketIndex != PerpPosition.PerpMarketIndexUnset; } + + public unsettledFunding(perpMarket: PerpMarket): I80F48 { + if (this.basePositionLots > 0) { + return perpMarket.longFunding + .sub(this.longSettledFunding) + .mul(I80F48.fromString(this.basePositionLots.toString())); + } else if (this.basePositionLots < 0) { + return perpMarket.shortFunding + .sub(this.shortSettledFunding) + .mul(I80F48.fromString(this.basePositionLots.toString())); + } + return ZERO_I80F48(); + } + + public getEquity(perpMarket: PerpMarket): I80F48 { + const lotsToQuote = I80F48.fromString( + perpMarket.baseLotSize.toString(), + ).mul(perpMarket.price); + + const baseLots = I80F48.fromNumber( + this.basePositionLots + this.takerBaseLots, + ); + + const unsettledFunding = this.unsettledFunding(perpMarket); + const takerQuote = I80F48.fromString( + new BN(this.takerQuoteLots).mul(perpMarket.quoteLotSize).toString(), + ); + const quoteCurrent = I80F48.fromString(this.quotePositionNative.toString()) + .sub(unsettledFunding) + .add(takerQuote); + + return baseLots.mul(lotsToQuote).add(quoteCurrent); + } + + public hasOpenOrders(): boolean { + return ( + this.asksBaseLots != 0 || + this.bidsBaseLots != 0 || + this.takerBaseLots != 0 || + this.takerQuoteLots != 0 + ); + } } export class PerpPositionDto { @@ -944,12 +1027,14 @@ export class PerpPositionDto { public asksBaseLots: BN, public takerBaseLots: BN, public takerQuoteLots: BN, + public longSettledFunding: I80F48Dto, + public shortSettledFunding: I80F48Dto, ) {} } export class PerpOo { static OrderMarketUnset = 65535; - static from(dto: PerpOoDto) { + static from(dto: PerpOoDto): PerpOo { return new PerpOo( dto.orderSide, dto.orderMarket, @@ -978,66 +1063,3 @@ export class HealthType { static maint = { maint: {} }; static init = { init: {} }; } - -export class MangoAccountData { - constructor( - public healthCache: HealthCache, - public initHealth: I80F48, - public maintHealth: I80F48, - public equity: Equity, - ) {} - - static from(event: { - healthCache: HealthCacheDto; - initHealth: I80F48Dto; - maintHealth: I80F48Dto; - equity: { - tokens: [{ tokenIndex: number; value: I80F48Dto }]; - perps: [{ perpMarketIndex: number; value: I80F48Dto }]; - }; - initHealthLiabs: I80F48Dto; - tokenAssets: any; - }) { - return new MangoAccountData( - HealthCache.fromDto(event.healthCache), - I80F48.from(event.initHealth), - I80F48.from(event.maintHealth), - Equity.from(event.equity), - ); - } -} - -export class Equity { - public constructor( - public tokens: TokenEquity[], - public perps: PerpEquity[], - ) {} - - static from(dto: EquityDto): Equity { - return new Equity( - dto.tokens.map( - (token) => new TokenEquity(token.tokenIndex, I80F48.from(token.value)), - ), - dto.perps.map( - (perpAccount) => - new PerpEquity( - perpAccount.perpMarketIndex, - I80F48.from(perpAccount.value), - ), - ), - ); - } -} - -export class TokenEquity { - public constructor(public tokenIndex: number, public value: I80F48) {} -} - -export class PerpEquity { - public constructor(public perpMarketIndex: number, public value: I80F48) {} -} - -export class EquityDto { - tokens: { tokenIndex: number; value: I80F48Dto }[]; - perps: { perpMarketIndex: number; value: I80F48Dto }[]; -} diff --git a/ts/client/src/accounts/oracle.ts b/ts/client/src/accounts/oracle.ts index b84642358..10420caea 100644 --- a/ts/client/src/accounts/oracle.ts +++ b/ts/client/src/accounts/oracle.ts @@ -102,7 +102,7 @@ export async function parseSwitchboardOracle( return parseSwitcboardOracleV1(accountInfo); } - throw new Error(`Unable to parse switchboard oracle ${accountInfo.owner}`); + throw new Error(`Should not be reached!`); } export function isSwitchboardOracle(accountInfo: AccountInfo): boolean { diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index ce00a0cfc..5bf8fa2d8 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -3,10 +3,12 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { PublicKey } from '@solana/web3.js'; import Big from 'big.js'; import { MangoClient } from '../client'; -import { U64_MAX_BN } from '../utils'; +import { As, U64_MAX_BN } from '../utils'; import { OracleConfig, QUOTE_DECIMALS } from './bank'; import { I80F48, I80F48Dto } from './I80F48'; +export type PerpMarketIndex = number & As<'perp-market-index'>; + export class PerpMarket { public name: string; public maintAssetWeight: I80F48; @@ -18,21 +20,23 @@ export class PerpMarket { public takerFee: I80F48; public minFunding: I80F48; public maxFunding: I80F48; + public longFunding: I80F48; + public shortFunding: I80F48; public openInterest: number; public seqNum: number; public feesAccrued: I80F48; priceLotsToUiConverter: number; baseLotsToUiConverter: number; quoteLotsToUiConverter: number; - public price: number; - public uiPrice: number; + public _price: I80F48; + public _uiPrice: number; static from( publicKey: PublicKey, obj: { group: PublicKey; - quoteTokenIndex: number; perpMarketIndex: number; + trustedMarket: number; name: number[]; oracle: PublicKey; oracleConfig: OracleConfig; @@ -55,7 +59,7 @@ export class PerpMarket { shortFunding: I80F48Dto; fundingLastUpdated: BN; openInterest: BN; - seqNum: any; // TODO: ts complains that this is unknown for whatever reason + seqNum: BN; feesAccrued: I80F48Dto; bump: number; baseDecimals: number; @@ -65,8 +69,8 @@ export class PerpMarket { return new PerpMarket( publicKey, obj.group, - obj.quoteTokenIndex, - obj.perpMarketIndex, + obj.perpMarketIndex as PerpMarketIndex, + obj.trustedMarket == 1, obj.name, obj.oracle, obj.oracleConfig, @@ -100,8 +104,8 @@ export class PerpMarket { constructor( public publicKey: PublicKey, public group: PublicKey, - public quoteTokenIndex: number, - public perpMarketIndex: number, + public perpMarketIndex: PerpMarketIndex, // TODO rename to marketIndex? + public trustedMarket: boolean, name: number[], public oracle: PublicKey, oracleConfig: OracleConfig, @@ -140,6 +144,8 @@ export class PerpMarket { this.takerFee = I80F48.from(takerFee); this.minFunding = I80F48.from(minFunding); this.maxFunding = I80F48.from(maxFunding); + this.longFunding = I80F48.from(longFunding); + this.shortFunding = I80F48.from(shortFunding); this.openInterest = openInterest.toNumber(); this.seqNum = seqNum.toNumber(); this.feesAccrued = I80F48.from(feesAccrued); @@ -159,6 +165,23 @@ export class PerpMarket { .toNumber(); } + get price(): I80F48 { + if (!this._price) { + throw new Error( + `Undefined price for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`, + ); + } + return this._price; + } + + get uiPrice(): number { + if (!this._uiPrice) { + throw new Error( + `Undefined price for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`, + ); + } + return this._uiPrice; + } public async loadAsks(client: MangoClient): Promise { const asks = await client.program.account.bookSide.fetch(this.asks); return BookSide.from(client, this, BookSideType.asks, asks); @@ -176,7 +199,10 @@ export class PerpMarket { return new PerpEventQueue(client, eventQueue.header, eventQueue.buf); } - public async loadFills(client: MangoClient, lastSeqNum: BN) { + public async loadFills( + client: MangoClient, + lastSeqNum: BN, + ): Promise<(OutEvent | FillEvent | LiquidateEvent)[]> { const eventQueue = await this.loadEventQueue(client); return eventQueue .eventsSince(lastSeqNum) @@ -189,13 +215,13 @@ export class PerpMarket { * @param asks * @returns returns funding rate per hour */ - public getCurrentFundingRate(bids: BookSide, asks: BookSide) { + public getCurrentFundingRate(bids: BookSide, asks: BookSide): number { const MIN_FUNDING = this.minFunding.toNumber(); const MAX_FUNDING = this.maxFunding.toNumber(); const bid = bids.getImpactPriceUi(new BN(this.impactQuantity)); const ask = asks.getImpactPriceUi(new BN(this.impactQuantity)); - const indexPrice = this.uiPrice; + const indexPrice = this._uiPrice; let funding; if (bid !== undefined && ask !== undefined) { @@ -284,7 +310,7 @@ export class BookSide { leafCount: number; nodes: unknown; }, - ) { + ): BookSide { return new BookSide( client, perpMarket, @@ -311,7 +337,6 @@ export class BookSide { public includeExpired = false, maxBookDelay?: number, ) { - // TODO why? Ask Daffy // Determine the maxTimestamp found on the book to use for tif // If maxBookDelay is not provided, use 3600 as a very large number maxBookDelay = maxBookDelay === undefined ? 3600 : maxBookDelay; @@ -329,7 +354,7 @@ export class BookSide { this.now = maxTimestamp; } - static getPriceFromKey(key: BN) { + static getPriceFromKey(key: BN): BN { return key.ushrn(64); } @@ -456,7 +481,7 @@ export class LeafNode { ) {} } export class InnerNode { - static from(obj: { children: [number] }) { + static from(obj: { children: [number] }): InnerNode { return new InnerNode(obj.children); } @@ -477,7 +502,11 @@ export class PerpOrderType { } export class PerpOrder { - static from(perpMarket: PerpMarket, leafNode: LeafNode, type: BookSideType) { + static from( + perpMarket: PerpMarket, + leafNode: LeafNode, + type: BookSideType, + ): PerpOrder { const side = type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask; const price = BookSide.getPriceFromKey(leafNode.key); @@ -555,7 +584,7 @@ export class PerpEventQueue { ), ); } - throw new Error(`Unknown event with eventType ${event.eventType}`); + throw new Error(`Unknown event with eventType ${event.eventType}!`); }); } diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index bb07fd09d..56324e064 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -4,9 +4,13 @@ import { Cluster, PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; import { MangoClient } from '../client'; import { SERUM3_PROGRAM_ID } from '../constants'; +import { As } from '../utils'; +import { TokenIndex } from './bank'; import { Group } from './group'; import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48'; +export type MarketIndex = number & As<'market-index'>; + export class Serum3Market { public name: string; static from( @@ -26,12 +30,12 @@ export class Serum3Market { return new Serum3Market( publicKey, obj.group, - obj.baseTokenIndex, - obj.quoteTokenIndex, + obj.baseTokenIndex as TokenIndex, + obj.quoteTokenIndex as TokenIndex, obj.name, obj.serumProgram, obj.serumMarketExternal, - obj.marketIndex, + obj.marketIndex as MarketIndex, obj.registrationTime, ); } @@ -39,12 +43,12 @@ export class Serum3Market { constructor( public publicKey: PublicKey, public group: PublicKey, - public baseTokenIndex: number, - public quoteTokenIndex: number, + public baseTokenIndex: TokenIndex, + public quoteTokenIndex: TokenIndex, name: number[], public serumProgram: PublicKey, public serumMarketExternal: PublicKey, - public marketIndex: number, + public marketIndex: MarketIndex, public registrationTime: BN, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; @@ -58,19 +62,7 @@ export class Serum3Market { */ maxBidLeverage(group: Group): number { const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex); - if (!baseBank) { - throw new Error( - `bank for base token with index ${this.baseTokenIndex} not found`, - ); - } - const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex); - if (!quoteBank) { - throw new Error( - `bank for quote token with index ${this.quoteTokenIndex} not found`, - ); - } - if ( quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48()) ) { @@ -90,18 +82,7 @@ export class Serum3Market { */ maxAskLeverage(group: Group): number { const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex); - if (!baseBank) { - throw new Error( - `bank for base token with index ${this.baseTokenIndex} not found`, - ); - } - const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex); - if (!quoteBank) { - throw new Error( - `bank for quote token with index ${this.quoteTokenIndex} not found`, - ); - } if ( baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48()) @@ -115,28 +96,18 @@ export class Serum3Market { } public async loadBids(client: MangoClient, group: Group): Promise { - const serum3MarketExternal = group.serum3MarketExternalsMap.get( - this.serumMarketExternal.toBase58(), + const serum3MarketExternal = group.getSerum3ExternalMarket( + this.serumMarketExternal, ); - if (!serum3MarketExternal) { - throw new Error( - `Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`, - ); - } return await serum3MarketExternal.loadBids( client.program.provider.connection, ); } public async loadAsks(client: MangoClient, group: Group): Promise { - const serum3MarketExternal = group.serum3MarketExternalsMap.get( - this.serumMarketExternal.toBase58(), + const serum3MarketExternal = group.getSerum3ExternalMarket( + this.serumMarketExternal, ); - if (!serum3MarketExternal) { - throw new Error( - `Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`, - ); - } return await serum3MarketExternal.loadAsks( client.program.provider.connection, ); diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 0b4fb1fc2..d177e9b19 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -24,14 +24,13 @@ import { TransactionSignature, } from '@solana/web3.js'; import bs58 from 'bs58'; -import { Bank, MintInfo } from './accounts/bank'; +import { Bank, MintInfo, TokenIndex } from './accounts/bank'; import { Group } from './accounts/group'; import { I80F48 } from './accounts/I80F48'; import { MangoAccount, - MangoAccountData, - TokenPosition, PerpPosition, + TokenPosition, } from './accounts/mangoAccount'; import { StubOracle } from './accounts/oracle'; import { @@ -39,6 +38,7 @@ import { OutEvent, PerpEventQueue, PerpMarket, + PerpMarketIndex, PerpOrderSide, PerpOrderType, } from './accounts/perp'; @@ -59,7 +59,6 @@ import { I64_MAX_BN, toNativeDecimals, } from './utils'; -import { simulate } from './utils/anchor'; import { sendTransaction } from './utils/rpc'; enum AccountRetriever { @@ -70,7 +69,6 @@ enum AccountRetriever { export type IdsSource = 'api' | 'static' | 'get-program-accounts'; // TODO: replace ui values with native as input wherever possible -// TODO: replace token/market names with token or market indices export class MangoClient { private postSendTxCallback?: ({ txid }) => void; private prioritizationFee: number; @@ -405,7 +403,7 @@ export class MangoClient { public async getMintInfoForTokenIndex( group: Group, - tokenIndex: number, + tokenIndex: TokenIndex, ): Promise { const tokenIndexBuf = Buffer.alloc(2); tokenIndexBuf.writeUInt16LE(tokenIndex); @@ -649,21 +647,27 @@ export class MangoClient { ); } - public async getMangoAccount(mangoAccount: MangoAccount) { + public async getMangoAccount( + mangoAccount: MangoAccount, + ): Promise { return MangoAccount.from( mangoAccount.publicKey, await this.program.account.mangoAccount.fetch(mangoAccount.publicKey), ); } - public async getMangoAccountForPublicKey(mangoAccountPk: PublicKey) { + public async getMangoAccountForPublicKey( + mangoAccountPk: PublicKey, + ): Promise { return MangoAccount.from( mangoAccountPk, await this.program.account.mangoAccount.fetch(mangoAccountPk), ); } - public async getMangoAccountWithSlot(mangoAccountPk: PublicKey) { + public async getMangoAccountWithSlot( + mangoAccountPk: PublicKey, + ): Promise<{ slot: number; value: MangoAccount } | undefined> { const resp = await this.program.provider.connection.getAccountInfoAndContext( mangoAccountPk, @@ -753,52 +757,6 @@ export class MangoClient { ); } - public async computeAccountData( - group: Group, - mangoAccount: MangoAccount, - ): Promise { - const healthRemainingAccounts: PublicKey[] = - this.buildHealthRemainingAccounts( - AccountRetriever.Fixed, - group, - [mangoAccount], - [], - [], - ); - - // Use our custom simulate fn in utils/anchor.ts so signing the tx is not required - this.program.provider.simulate = simulate; - - const res = await this.program.methods - .computeAccountData() - .accounts({ - group: group.publicKey, - account: mangoAccount.publicKey, - }) - .remainingAccounts( - healthRemainingAccounts.map( - (pk) => - ({ - pubkey: pk, - isWritable: false, - isSigner: false, - } as AccountMeta), - ), - ) - .simulate(); - - if (res.events) { - const accountDataEvent = res?.events.find( - (event) => (event.name = 'MangoAccountData'), - ); - return accountDataEvent - ? MangoAccountData.from(accountDataEvent.data as any) - : undefined; - } else { - return undefined; - } - } - public async tokenDeposit( group: Group, mangoAccount: MangoAccount, @@ -820,7 +778,7 @@ export class MangoClient { mangoAccount: MangoAccount, mintPk: PublicKey, nativeAmount: number, - ) { + ): Promise { const bank = group.getFirstBankByMint(mintPk); const tokenAccountPk = await getAssociatedTokenAddress( @@ -1148,19 +1106,19 @@ export class MangoClient { orderType: Serum3OrderType, clientOrderId: number, limit: number, - ) { + ): Promise { const serum3Market = group.serum3MarketsMapByExternal.get( externalMarketPk.toBase58(), )!; - if (!mangoAccount.findSerum3Account(serum3Market.marketIndex)) { + if (!mangoAccount.getSerum3Account(serum3Market.marketIndex)) { await this.serum3CreateOpenOrders( group, mangoAccount, serum3Market.serumMarketExternal, ); - await mangoAccount.reload(this, group); + await mangoAccount.reload(this); } - const serum3MarketExternal = group.serum3MarketExternalsMap.get( + const serum3MarketExternal = group.serum3ExternalMarketsMap.get( externalMarketPk.toBase58(), )!; const serum3MarketExternalVaultSigner = @@ -1182,13 +1140,17 @@ export class MangoClient { const limitPrice = serum3MarketExternal.priceNumberToLots(price); const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size); const maxQuoteQuantity = serum3MarketExternal.decoded.quoteLotSize - .mul(new BN(1 + group.getFeeRate(orderType === Serum3OrderType.postOnly))) + .mul( + new BN( + 1 + group.getSerum3FeeRates(orderType === Serum3OrderType.postOnly), + ), + ) .mul( serum3MarketExternal .baseSizeNumberToLots(size) .mul(serum3MarketExternal.priceNumberToLots(price)), ); - const payerTokenIndex = (() => { + const payerTokenIndex = ((): TokenIndex => { if (side == Serum3Side.bid) { return serum3Market.quoteTokenIndex; } else { @@ -1211,7 +1173,7 @@ export class MangoClient { group: group.publicKey, account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, - openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex) + openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex) ?.openOrders, serumMarket: serum3Market.publicKey, serumProgram: SERUM3_PROGRAM_ID[this.cluster], @@ -1249,12 +1211,12 @@ export class MangoClient { mangoAccount: MangoAccount, externalMarketPk: PublicKey, limit: number, - ) { + ): Promise { const serum3Market = group.serum3MarketsMapByExternal.get( externalMarketPk.toBase58(), )!; - const serum3MarketExternal = group.serum3MarketExternalsMap.get( + const serum3MarketExternal = group.serum3ExternalMarketsMap.get( externalMarketPk.toBase58(), )!; @@ -1264,7 +1226,7 @@ export class MangoClient { group: group.publicKey, account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, - openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex) + openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex) ?.openOrders, serumMarket: serum3Market.publicKey, serumProgram: SERUM3_PROGRAM_ID[this.cluster], @@ -1293,7 +1255,7 @@ export class MangoClient { const serum3Market = group.serum3MarketsMapByExternal.get( externalMarketPk.toBase58(), )!; - const serum3MarketExternal = group.serum3MarketExternalsMap.get( + const serum3MarketExternal = group.serum3ExternalMarketsMap.get( externalMarketPk.toBase58(), )!; const serum3MarketExternalVaultSigner = @@ -1309,7 +1271,7 @@ export class MangoClient { group: group.publicKey, account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, - openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex) + openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex) ?.openOrders, serumMarket: serum3Market.publicKey, serumProgram: SERUM3_PROGRAM_ID[this.cluster], @@ -1349,7 +1311,7 @@ export class MangoClient { externalMarketPk.toBase58(), )!; - const serum3MarketExternal = group.serum3MarketExternalsMap.get( + const serum3MarketExternal = group.serum3ExternalMarketsMap.get( externalMarketPk.toBase58(), )!; @@ -1358,7 +1320,7 @@ export class MangoClient { .accounts({ group: group.publicKey, account: mangoAccount.publicKey, - openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex) + openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex) ?.openOrders, serumMarket: serum3Market.publicKey, serumProgram: SERUM3_PROGRAM_ID[this.cluster], @@ -1495,7 +1457,7 @@ export class MangoClient { async perpEditMarket( group: Group, - perpMarketName: string, + perpMarketIndex: PerpMarketIndex, oracle: PublicKey, oracleConfFilter: number, baseDecimals: number, @@ -1516,7 +1478,7 @@ export class MangoClient { settleFeeAmountThreshold: number, settleFeeFractionLowHealth: number, ): Promise { - const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); return await this.program.methods .perpEditMarket( @@ -1554,9 +1516,9 @@ export class MangoClient { async perpCloseMarket( group: Group, - perpMarketName: string, + perpMarketIndex: PerpMarketIndex, ): Promise { - const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); return await this.program.methods .perpCloseMarket() @@ -1594,9 +1556,9 @@ export class MangoClient { async perpDeactivatePosition( group: Group, mangoAccount: MangoAccount, - perpMarketName: string, + perpMarketIndex: PerpMarketIndex, ): Promise { - const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const healthRemainingAccounts: PublicKey[] = this.buildHealthRemainingAccounts( AccountRetriever.Fixed, @@ -1625,7 +1587,7 @@ export class MangoClient { async perpPlaceOrder( group: Group, mangoAccount: MangoAccount, - perpMarketName: string, + perpMarketIndex: PerpMarketIndex, side: PerpOrderSide, price: number, quantity: number, @@ -1635,14 +1597,14 @@ export class MangoClient { expiryTimestamp: number, limit: number, ): Promise { - const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const healthRemainingAccounts: PublicKey[] = this.buildHealthRemainingAccounts( AccountRetriever.Fixed, group, [mangoAccount], // Settlement token bank, because a position for it may be created - [group.getFirstBankByTokenIndex(0)], + [group.getFirstBankByTokenIndex(0 as TokenIndex)], [perpMarket], ); const ix = await this.program.methods @@ -1689,10 +1651,10 @@ export class MangoClient { async perpCancelAllOrders( group: Group, mangoAccount: MangoAccount, - perpMarketName: string, + perpMarketIndex: PerpMarketIndex, limit: number, ): Promise { - const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const ix = await this.program.methods .perpCancelAllOrders(limit) .accounts({ @@ -1717,11 +1679,11 @@ export class MangoClient { async perpConsumeEvents( group: Group, - perpMarketName: string, + perpMarketIndex: PerpMarketIndex, accounts: PublicKey[], limit: number, ): Promise { - const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); return await this.program.methods .perpConsumeEvents(new BN(limit)) .accounts({ @@ -1740,10 +1702,10 @@ export class MangoClient { async perpConsumeAllEvents( group: Group, - perpMarketName: string, + perpMarketIndex: PerpMarketIndex, ): Promise { const limit = 8; - const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const eventQueue = await perpMarket.loadEventQueue(this); const unconsumedEvents = eventQueue.getUnconsumedEvents(); while (unconsumedEvents.length > 0) { @@ -1762,12 +1724,12 @@ export class MangoClient { case PerpEventQueue.LIQUIDATE_EVENT_TYPE: return []; default: - throw new Error(`Unknown event with eventType ${ev.eventType}`); + throw new Error(`Unknown event with eventType ${ev.eventType}!`); } }) .flat(); - await this.perpConsumeEvents(group, perpMarketName, accounts, limit); + await this.perpConsumeEvents(group, perpMarketIndex, accounts, limit); } } @@ -1793,8 +1755,6 @@ export class MangoClient { const inputBank: Bank = group.getFirstBankByMint(inputMintPk); const outputBank: Bank = group.getFirstBankByMint(outputMintPk); - if (!inputBank || !outputBank) throw new Error('Invalid token'); - const healthRemainingAccounts: PublicKey[] = this.buildHealthRemainingAccounts( AccountRetriever.Fixed, @@ -1944,12 +1904,15 @@ export class MangoClient { ); } - async updateIndexAndRate(group: Group, mintPk: PublicKey) { + async updateIndexAndRate( + group: Group, + mintPk: PublicKey, + ): Promise { // TODO: handle updating multiple banks const bank = group.getFirstBankByMint(mintPk); const mintInfo = group.mintInfosMapByMint.get(mintPk.toString())!; - await this.program.methods + return await this.program.methods .tokenUpdateIndexAndRate() .accounts({ group: group.publicKey, @@ -1976,7 +1939,7 @@ export class MangoClient { assetMintPk: PublicKey, liabMintPk: PublicKey, maxLiabTransfer: number, - ) { + ): Promise { const assetBank: Bank = group.getFirstBankByMint(assetMintPk); const liabBank: Bank = group.getFirstBankByMint(liabMintPk); @@ -2024,7 +1987,11 @@ export class MangoClient { ); } - async altSet(group: Group, addressLookupTable: PublicKey, index: number) { + async altSet( + group: Group, + addressLookupTable: PublicKey, + index: number, + ): Promise { const ix = await this.program.methods .altSet(index) .accounts({ @@ -2049,7 +2016,7 @@ export class MangoClient { addressLookupTable: PublicKey, index: number, pks: PublicKey[], - ) { + ): Promise { return await this.program.methods .altExtend(index, pks) .accounts({ @@ -2070,9 +2037,6 @@ export class MangoClient { opts: any = {}, getIdsFromApi: IdsSource = 'api', ): MangoClient { - // TODO: use IDL on chain or in repository? decide... - // Alternatively we could fetch IDL from chain. - // const idl = await Program.fetchIdl(MANGO_V4_ID, provider); const idl = IDL; return new MangoClient( @@ -2108,8 +2072,7 @@ export class MangoClient { /// private - // todo make private - public buildHealthRemainingAccounts( + private buildHealthRemainingAccounts( retriever: AccountRetriever, group: Group, mangoAccounts: MangoAccount[], @@ -2133,8 +2096,7 @@ export class MangoClient { } } - // todo make private - public buildFixedAccountRetrieverHealthAccounts( + private buildFixedAccountRetrieverHealthAccounts( group: Group, mangoAccount: MangoAccount, // Banks and perpMarkets for whom positions don't exist on mango account, @@ -2207,8 +2169,7 @@ export class MangoClient { return healthRemainingAccounts; } - // todo make private - public buildScanningAccountRetrieverHealthAccounts( + private buildScanningAccountRetrieverHealthAccounts( group: Group, mangoAccounts: MangoAccount[], banks: Bank[], @@ -2216,7 +2177,7 @@ export class MangoClient { ): PublicKey[] { const healthRemainingAccounts: PublicKey[] = []; - let tokenIndices: number[] = []; + let tokenIndices: TokenIndex[] = []; for (const mangoAccount of mangoAccounts) { tokenIndices.push( ...mangoAccount.tokens @@ -2241,7 +2202,7 @@ export class MangoClient { ...mintInfos.map((mintInfo) => mintInfo.oracle), ); - const perpIndices: number[] = []; + const perpIndices: PerpMarketIndex[] = []; for (const mangoAccount of mangoAccounts) { perpIndices.push( ...mangoAccount.perps diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index 36d5395e4..7f315ef66 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -2,6 +2,7 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { Cluster, Connection, Keypair } from '@solana/web3.js'; import fs from 'fs'; import { Group } from '../accounts/group'; +import { HealthCache } from '../accounts/healthCache'; import { HealthType, MangoAccount } from '../accounts/mangoAccount'; import { PerpMarket } from '../accounts/perp'; import { Serum3Market } from '../accounts/serum3'; @@ -26,7 +27,7 @@ async function debugUser( ) { console.log(mangoAccount.toString(group)); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); console.log( 'buildFixedAccountRetrieverHealthAccounts ' + @@ -45,42 +46,52 @@ async function debugUser( ); console.log( 'mangoAccount.getEquity() ' + - toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()), + toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()), ); console.log( 'mangoAccount.getHealth(HealthType.init) ' + - toUiDecimalsForQuote(mangoAccount.getHealth(HealthType.init)!.toNumber()), + toUiDecimalsForQuote( + mangoAccount.getHealth(group, HealthType.init)!.toNumber(), + ), + ); + console.log( + 'HealthCache.fromMangoAccount(group,mangoAccount).health(HealthType.init) ' + + toUiDecimalsForQuote( + HealthCache.fromMangoAccount(group, mangoAccount) + .health(HealthType.init) + .toNumber(), + ), ); console.log( 'mangoAccount.getHealthRatio(HealthType.init) ' + - mangoAccount.getHealthRatio(HealthType.init)!.toNumber(), + mangoAccount.getHealthRatio(group, HealthType.init)!.toNumber(), ); console.log( 'mangoAccount.getHealthRatioUi(HealthType.init) ' + - mangoAccount.getHealthRatioUi(HealthType.init), + mangoAccount.getHealthRatioUi(group, HealthType.init), ); console.log( 'mangoAccount.getHealthRatio(HealthType.maint) ' + - mangoAccount.getHealthRatio(HealthType.maint)!.toNumber(), + mangoAccount.getHealthRatio(group, HealthType.maint)!.toNumber(), ); console.log( 'mangoAccount.getHealthRatioUi(HealthType.maint) ' + - mangoAccount.getHealthRatioUi(HealthType.maint), + mangoAccount.getHealthRatioUi(group, HealthType.maint), ); console.log( 'mangoAccount.getCollateralValue() ' + - toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()), + toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)!.toNumber()), ); console.log( 'mangoAccount.getAssetsValue() ' + toUiDecimalsForQuote( - mangoAccount.getAssetsValue(HealthType.init)!.toNumber(), + mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(), ), ); console.log( 'mangoAccount.getLiabsValue() ' + toUiDecimalsForQuote( - mangoAccount.getLiabsValue(HealthType.init)!.toNumber(), + mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(), ), ); @@ -223,9 +234,9 @@ async function main() { for (const mangoAccount of mangoAccounts) { console.log(`MangoAccount ${mangoAccount.publicKey}`); - // if (mangoAccount.name === 'PnL Test') { - await debugUser(client, group, mangoAccount); - // } + if (mangoAccount.name === 'PnL Test') { + await debugUser(client, group, mangoAccount); + } } } diff --git a/ts/client/src/ids.ts b/ts/client/src/ids.ts index fec0dd482..004ad2f7a 100644 --- a/ts/client/src/ids.ts +++ b/ts/client/src/ids.ts @@ -51,7 +51,7 @@ export class Id { static fromIdsByName(name: string): Id { const groupConfig = ids.groups.find((id) => id['name'] === name); - if (!groupConfig) throw new Error(`Unable to find group config ${name}`); + if (!groupConfig) throw new Error(`No group config ${name} found in Ids!`); return new Id( groupConfig.cluster as Cluster, groupConfig.name, @@ -71,7 +71,7 @@ export class Id { (id) => id['publicKey'] === groupPk.toString(), ); if (!groupConfig) - throw new Error(`Unable to find group config ${groupPk.toString()}`); + throw new Error(`No group config ${groupPk.toString()} found in Ids!`); return new Id( groupConfig.cluster as Cluster, groupConfig.name, diff --git a/ts/client/src/scripts/devnet-admin.ts b/ts/client/src/scripts/devnet-admin.ts index 5bf5dc558..d706407f9 100644 --- a/ts/client/src/scripts/devnet-admin.ts +++ b/ts/client/src/scripts/devnet-admin.ts @@ -567,7 +567,6 @@ async function main() { // ); // console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); - // TODO decide on what keys should go in console.log(`ALT: extending manually with bank publick keys and oracles`); const extendIx = AddressLookupTableProgram.extendLookupTable({ lookupTable: createIx[1], diff --git a/ts/client/src/scripts/devnet-user.ts b/ts/client/src/scripts/devnet-user.ts index 4c4e5b25e..fa58ee709 100644 --- a/ts/client/src/scripts/devnet-user.ts +++ b/ts/client/src/scripts/devnet-user.ts @@ -78,10 +78,10 @@ async function main() { console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(mangoAccount.toString(group)); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); // set delegate, and change name - if (false) { + if (true) { console.log(`...changing mango account name, and setting a delegate`); const randomKey = new PublicKey( '4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo', @@ -93,7 +93,7 @@ async function main() { 'my_changed_name', randomKey, ); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); console.log(mangoAccount.toString()); console.log(`...resetting mango account name, and re-setting a delegate`); @@ -103,7 +103,7 @@ async function main() { 'my_mango_account', PublicKey.default, ); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); console.log(mangoAccount.toString()); } @@ -113,7 +113,7 @@ async function main() { `...expanding mango account to have serum3 and perp position slots`, ); await client.expandMangoAccount(group, mangoAccount, 8, 8, 8, 8); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); } // deposit and withdraw @@ -126,7 +126,7 @@ async function main() { new PublicKey(DEVNET_MINTS.get('USDC')!), 50, ); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); await client.tokenDeposit( group, @@ -134,7 +134,7 @@ async function main() { new PublicKey(DEVNET_MINTS.get('SOL')!), 1, ); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); await client.tokenDeposit( group, @@ -142,7 +142,7 @@ async function main() { new PublicKey(DEVNET_MINTS.get('MNGO')!), 1, ); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); console.log(`...withdrawing 1 USDC`); await client.tokenWithdraw( @@ -152,7 +152,7 @@ async function main() { 1, true, ); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); console.log(`...depositing 0.0005 BTC`); await client.tokenDeposit( @@ -161,7 +161,7 @@ async function main() { new PublicKey(DEVNET_MINTS.get('BTC')!), 0.0005, ); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); console.log(mangoAccount.toString(group)); } catch (error) { @@ -171,12 +171,6 @@ async function main() { if (true) { // serum3 - const serum3Market = group.serum3MarketsMapByExternal.get( - DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!, - ); - const serum3MarketExternal = group.serum3MarketExternalsMap.get( - DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!, - ); const asks = await group.loadSerum3AsksForMarket( client, DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, @@ -205,7 +199,7 @@ async function main() { Date.now(), 10, ); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); price = lowestAsk.price + lowestAsk.price / 2; qty = 0.0001; @@ -224,7 +218,7 @@ async function main() { Date.now(), 10, ); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); price = highestBid.price - highestBid.price / 2; qty = 0.0001; @@ -291,33 +285,27 @@ async function main() { } if (true) { - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); console.log( '...mangoAccount.getEquity() ' + - toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()), + toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()), ); console.log( '...mangoAccount.getCollateralValue() ' + - toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()), - ); - console.log( - '...mangoAccount.accountData["healthCache"].health(HealthType.init) ' + toUiDecimalsForQuote( - mangoAccount - .accountData!['healthCache'].health(HealthType.init) - .toNumber(), + mangoAccount.getCollateralValue(group)!.toNumber(), ), ); console.log( '...mangoAccount.getAssetsVal() ' + toUiDecimalsForQuote( - mangoAccount.getAssetsValue(HealthType.init)!.toNumber(), + mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(), ), ); console.log( '...mangoAccount.getLiabsVal() ' + toUiDecimalsForQuote( - mangoAccount.getLiabsValue(HealthType.init)!.toNumber(), + mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(), ), ); console.log( @@ -400,10 +388,11 @@ async function main() { // perps if (true) { let sig; + const perpMarket = group.getPerpMarketByName('BTC-PERP'); const orders = await mangoAccount.loadPerpOpenOrdersForMarket( client, group, - 'BTC-PERP', + perpMarket.perpMarketIndex, ); for (const order of orders) { console.log( @@ -411,7 +400,12 @@ async function main() { ); } console.log(`...cancelling all perp orders`); - sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); + sig = await client.perpCancelAllOrders( + group, + mangoAccount, + perpMarket.perpMarketIndex, + 10, + ); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); // scenario 1 @@ -423,7 +417,7 @@ async function main() { Math.floor(Math.random() * 100); const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi( group, - 'BTC-PERP', + perpMarket.perpMarketIndex, 1, ); const baseQty = quoteQty / price; @@ -433,7 +427,7 @@ async function main() { const sig = await client.perpPlaceOrder( group, mangoAccount, - 'BTC-PERP', + perpMarket.perpMarketIndex, PerpOrderSide.bid, price, baseQty, @@ -448,7 +442,12 @@ async function main() { console.log(error); } console.log(`...cancelling all perp orders`); - sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); + sig = await client.perpCancelAllOrders( + group, + mangoAccount, + perpMarket.perpMarketIndex, + 10, + ); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); // bid max perp + some @@ -458,7 +457,11 @@ async function main() { group.banksMapByName.get('BTC')![0].uiPrice! - Math.floor(Math.random() * 100); const quoteQty = - mangoAccount.getMaxQuoteForPerpBidUi(group, 'BTC-PERP', 1) * 1.02; + mangoAccount.getMaxQuoteForPerpBidUi( + group, + perpMarket.perpMarketIndex, + 1, + ) * 1.02; const baseQty = quoteQty / price; console.log( `...placing max qty * 1.02 perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, @@ -466,7 +469,7 @@ async function main() { const sig = await client.perpPlaceOrder( group, mangoAccount, - 'BTC-PERP', + perpMarket.perpMarketIndex, PerpOrderSide.bid, price, baseQty, @@ -487,7 +490,11 @@ async function main() { const price = group.banksMapByName.get('BTC')![0].uiPrice! + Math.floor(Math.random() * 100); - const baseQty = mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1); + const baseQty = mangoAccount.getMaxBaseForPerpAskUi( + group, + perpMarket.perpMarketIndex, + 1, + ); const quoteQty = baseQty * price; console.log( `...placing max qty perp ask clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, @@ -495,7 +502,7 @@ async function main() { const sig = await client.perpPlaceOrder( group, mangoAccount, - 'BTC-PERP', + perpMarket.perpMarketIndex, PerpOrderSide.ask, price, baseQty, @@ -517,7 +524,11 @@ async function main() { group.banksMapByName.get('BTC')![0].uiPrice! + Math.floor(Math.random() * 100); const baseQty = - mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1) * 1.02; + mangoAccount.getMaxBaseForPerpAskUi( + group, + perpMarket.perpMarketIndex, + 1, + ) * 1.02; const quoteQty = baseQty * price; console.log( `...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, @@ -525,7 +536,7 @@ async function main() { const sig = await client.perpPlaceOrder( group, mangoAccount, - 'BTC-PERP', + perpMarket.perpMarketIndex, PerpOrderSide.ask, price, baseQty, @@ -541,7 +552,12 @@ async function main() { } console.log(`...cancelling all perp orders`); - sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); + sig = await client.perpCancelAllOrders( + group, + mangoAccount, + perpMarket.perpMarketIndex, + 10, + ); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); // // scenario 2 @@ -553,8 +569,8 @@ async function main() { // const sig = await client.perpPlaceOrder( // group, // mangoAccount, - // 'BTC-PERP', - // PerpOrderSide.bid, + // perpMarket.perpMarketIndex, + // PerpOrderSide.bid, // price, // 0.01, // price * 0.01, @@ -574,8 +590,8 @@ async function main() { // const sig = await client.perpPlaceOrder( // group, // mangoAccount, - // 'BTC-PERP', - // PerpOrderSide.ask, + // perpMarket.perpMarketIndex, + // PerpOrderSide.ask, // price, // 0.01, // price * 0.011, @@ -590,11 +606,9 @@ async function main() { // } // // // should be able to cancel them : know bug // // console.log(`...cancelling all perp orders`); - // // sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); + // // sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10); // // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - const perpMarket = group.perpMarketsMap.get('BTC-PERP')!; - const bids: BookSide = await perpMarket?.loadBids(client)!; console.log(`bids - ${Array.from(bids.items())}`); const asks: BookSide = await perpMarket?.loadAsks(client)!; @@ -615,7 +629,7 @@ async function main() { // make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position await group.reloadAll(client); - await mangoAccount.reload(client, group); + await mangoAccount.reload(client); console.log(`${mangoAccount.toString(group)}`); } diff --git a/ts/client/src/utils.ts b/ts/client/src/utils.ts index d6f616b74..aa7e18cdf 100644 --- a/ts/client/src/utils.ts +++ b/ts/client/src/utils.ts @@ -23,7 +23,13 @@ import { PerpMarket } from './accounts/perp'; export const U64_MAX_BN = new BN('18446744073709551615'); export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64); -export function debugAccountMetas(ams: AccountMeta[]) { +// https://stackoverflow.com/questions/70261755/user-defined-type-guard-function-and-type-narrowing-to-more-specific-type/70262876#70262876 +export declare abstract class As { + private static readonly $as$: unique symbol; + private [As.$as$]: Record; +} + +export function debugAccountMetas(ams: AccountMeta[]): void { for (const am of ams) { console.log( `${am.pubkey.toBase58()}, isSigner: ${am.isSigner @@ -39,7 +45,7 @@ export function debugHealthAccounts( group: Group, mangoAccount: MangoAccount, publicKeys: PublicKey[], -) { +): void { const banks = new Map( Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [ banks[0].publicKey.toBase58(), @@ -66,10 +72,12 @@ export function debugHealthAccounts( }), ); const perps = new Map( - Array.from(group.perpMarketsMap.values()).map((perpMarket: PerpMarket) => [ - perpMarket.publicKey.toBase58(), - `${perpMarket.name} perp market`, - ]), + Array.from(group.perpMarketsMapByName.values()).map( + (perpMarket: PerpMarket) => [ + perpMarket.publicKey.toBase58(), + `${perpMarket.name} perp market`, + ], + ), ); publicKeys.map((pk) => { @@ -126,7 +134,7 @@ export async function getAssociatedTokenAddress( associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, ): Promise { if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer())) - throw new Error('TokenOwnerOffCurve'); + throw new Error('TokenOwnerOffCurve!'); const [address] = await PublicKey.findProgramAddress( [owner.toBuffer(), programId.toBuffer(), mint.toBuffer()], diff --git a/ts/client/src/utils/anchor.ts b/ts/client/src/utils/anchor.ts deleted file mode 100644 index 230404299..000000000 --- a/ts/client/src/utils/anchor.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - simulateTransaction, - SuccessfulTxSimulationResponse, -} from '@project-serum/anchor/dist/cjs/utils/rpc'; -import { - Signer, - PublicKey, - Transaction, - Commitment, - SimulatedTransactionResponse, -} from '@solana/web3.js'; - -class SimulateError extends Error { - constructor( - readonly simulationResponse: SimulatedTransactionResponse, - message?: string, - ) { - super(message); - } -} - -export async function simulate( - tx: Transaction, - signers?: Signer[], - commitment?: Commitment, - includeAccounts?: boolean | PublicKey[], -): Promise { - tx.feePayer = this.wallet.publicKey; - tx.recentBlockhash = ( - await this.connection.getLatestBlockhash( - commitment ?? this.connection.commitment, - ) - ).blockhash; - - const result = await simulateTransaction(this.connection, tx); - - if (result.value.err) { - throw new SimulateError(result.value); - } - - return result.value; -} diff --git a/ts/client/src/utils/rpc.ts b/ts/client/src/utils/rpc.ts index e2b2ad074..e8a7e9b3c 100644 --- a/ts/client/src/utils/rpc.ts +++ b/ts/client/src/utils/rpc.ts @@ -10,7 +10,7 @@ export async function sendTransaction( ixs: TransactionInstruction[], alts: AddressLookupTableAccount[], opts: any = {}, -) { +): Promise { const connection = provider.connection; const latestBlockhash = await connection.getLatestBlockhash( opts.preflightCommitment, From 4b52d9a0725ac4ecb3f88d227c57566beaa5344f Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Fri, 30 Sep 2022 12:16:24 +0200 Subject: [PATCH 28/39] ts: simplify getOrCreateMangoAccount (#256) Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- ts/client/src/client.ts | 45 +++++-------------- .../example1-create-liquidation-candidate.ts | 11 +---- .../scripts/archive/example1-flash-loan.ts | 5 +-- ts/client/src/scripts/archive/example1-ob.ts | 5 +-- .../scripts/archive/example1-user-account.ts | 5 +-- .../src/scripts/archive/example1-user2.ts | 5 +-- .../src/scripts/archive/mb-flash-loan.ts | 5 +-- ts/client/src/scripts/devnet-user-2.ts | 5 +-- ts/client/src/scripts/devnet-user.ts | 5 +-- ts/client/src/scripts/mb-example1-admin.ts | 5 +-- ts/client/src/scripts/mb-example1-user.ts | 5 +-- 11 files changed, 22 insertions(+), 79 deletions(-) diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index d177e9b19..619c39dfa 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -505,41 +505,18 @@ export class MangoClient { // MangoAccount - public async getOrCreateMangoAccount( - group: Group, - ownerPk: PublicKey, - accountNumber?: number, - name?: string, - ): Promise { - // TODO: this function discards accountSize and name when the account exists already! - // TODO: this function always creates accounts for this.program.owner, and not - // ownerPk! It needs to get passed a keypair, and we need to add - // createMangoAccountForOwner - if (accountNumber === undefined) { - // Get any MangoAccount - // TODO: should probably sort by accountNum for deterministic output! - let mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk); - if (mangoAccounts.length === 0) { - await this.createMangoAccount(group, accountNumber, name); - mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk); - } - return mangoAccounts[0]; - } else { - let account = await this.getMangoAccountForOwner( - group, - ownerPk, - accountNumber, - ); - if (account === undefined) { - await this.createMangoAccount(group, accountNumber, name); - account = await this.getMangoAccountForOwner( - group, - ownerPk, - accountNumber, - ); - } - return account; + public async getOrCreateMangoAccount(group: Group): Promise { + const clientOwner = (this.program.provider as AnchorProvider).wallet + .publicKey; + let mangoAccounts = await this.getMangoAccountsForOwner( + group, + (this.program.provider as AnchorProvider).wallet.publicKey, + ); + if (mangoAccounts.length === 0) { + await this.createMangoAccount(group); + mangoAccounts = await this.getMangoAccountsForOwner(group, clientOwner); } + return mangoAccounts.sort((a, b) => a.accountNum - b.accountNum)[0]; } public async createMangoAccount( diff --git a/ts/client/src/scripts/archive/example1-create-liquidation-candidate.ts b/ts/client/src/scripts/archive/example1-create-liquidation-candidate.ts index 4598a00be..86d87212a 100644 --- a/ts/client/src/scripts/archive/example1-create-liquidation-candidate.ts +++ b/ts/client/src/scripts/archive/example1-create-liquidation-candidate.ts @@ -1,7 +1,6 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { Connection, Keypair } from '@solana/web3.js'; import fs from 'fs'; -import { AccountSize } from '../../accounts/mangoAccount'; import { MangoClient } from '../../client'; import { MANGO_V4_ID } from '../../constants'; @@ -45,10 +44,7 @@ async function main() { const group = await user1Client.getGroupForAdmin(admin.publicKey, GROUP_NUM); console.log(`Found group ${group.publicKey.toBase58()}`); - const user1MangoAccount = await user1Client.getOrCreateMangoAccount( - group, - user1.publicKey, - ); + const user1MangoAccount = await user1Client.getOrCreateMangoAccount(group); console.log(`...mangoAccount1 ${user1MangoAccount.publicKey}`); @@ -75,10 +71,7 @@ async function main() { ); console.log(`user2 ${user2Wallet.publicKey.toBase58()}`); - const user2MangoAccount = await user2Client.getOrCreateMangoAccount( - group, - user2.publicKey, - ); + const user2MangoAccount = await user2Client.getOrCreateMangoAccount(group); console.log(`...mangoAccount2 ${user2MangoAccount.publicKey}`); /// Increase usdc price temporarily to allow lots of borrows diff --git a/ts/client/src/scripts/archive/example1-flash-loan.ts b/ts/client/src/scripts/archive/example1-flash-loan.ts index 71ceacc1e..a6539e4cf 100644 --- a/ts/client/src/scripts/archive/example1-flash-loan.ts +++ b/ts/client/src/scripts/archive/example1-flash-loan.ts @@ -48,10 +48,7 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); - const mangoAccount = await client.getOrCreateMangoAccount( - group, - user.publicKey, - ); + const mangoAccount = await client.getOrCreateMangoAccount(group); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(mangoAccount.toString()); diff --git a/ts/client/src/scripts/archive/example1-ob.ts b/ts/client/src/scripts/archive/example1-ob.ts index d1d23aeb6..2e351bb9d 100644 --- a/ts/client/src/scripts/archive/example1-ob.ts +++ b/ts/client/src/scripts/archive/example1-ob.ts @@ -42,10 +42,7 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); - const mangoAccount = await client.getOrCreateMangoAccount( - group, - user.publicKey, - ); + const mangoAccount = await client.getOrCreateMangoAccount(group); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); // logging serum3 open orders for user diff --git a/ts/client/src/scripts/archive/example1-user-account.ts b/ts/client/src/scripts/archive/example1-user-account.ts index 892d77748..07f513689 100644 --- a/ts/client/src/scripts/archive/example1-user-account.ts +++ b/ts/client/src/scripts/archive/example1-user-account.ts @@ -43,10 +43,7 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); - const mangoAccount = await client.getOrCreateMangoAccount( - group, - user.publicKey, - ); + const mangoAccount = await client.getOrCreateMangoAccount(group); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); // log users tokens diff --git a/ts/client/src/scripts/archive/example1-user2.ts b/ts/client/src/scripts/archive/example1-user2.ts index 2ecb82680..a1ae7614b 100644 --- a/ts/client/src/scripts/archive/example1-user2.ts +++ b/ts/client/src/scripts/archive/example1-user2.ts @@ -48,10 +48,7 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); - const mangoAccount = await client.getOrCreateMangoAccount( - group, - user.publicKey, - ); + const mangoAccount = await client.getOrCreateMangoAccount(group); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(mangoAccount.toString()); diff --git a/ts/client/src/scripts/archive/mb-flash-loan.ts b/ts/client/src/scripts/archive/mb-flash-loan.ts index a75322267..79bb32e67 100644 --- a/ts/client/src/scripts/archive/mb-flash-loan.ts +++ b/ts/client/src/scripts/archive/mb-flash-loan.ts @@ -53,10 +53,7 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); - const mangoAccount = await client.getOrCreateMangoAccount( - group, - user.publicKey, - ); + const mangoAccount = await client.getOrCreateMangoAccount(group); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(`start balance \n${mangoAccount.toString(group)}`); diff --git a/ts/client/src/scripts/devnet-user-2.ts b/ts/client/src/scripts/devnet-user-2.ts index 14b532968..0f1e8d170 100644 --- a/ts/client/src/scripts/devnet-user-2.ts +++ b/ts/client/src/scripts/devnet-user-2.ts @@ -71,10 +71,7 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); - const mangoAccount = await client.getOrCreateMangoAccount( - group, - user.publicKey, - ); + const mangoAccount = await client.getOrCreateMangoAccount(group); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(mangoAccount.toString(group)); diff --git a/ts/client/src/scripts/devnet-user.ts b/ts/client/src/scripts/devnet-user.ts index fa58ee709..c379b5651 100644 --- a/ts/client/src/scripts/devnet-user.ts +++ b/ts/client/src/scripts/devnet-user.ts @@ -68,10 +68,7 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); - let mangoAccount = (await client.getOrCreateMangoAccount( - group, - user.publicKey, - ))!; + let mangoAccount = (await client.getOrCreateMangoAccount(group))!; if (!mangoAccount) { throw new Error(`MangoAccount not found for user ${user.publicKey}`); } diff --git a/ts/client/src/scripts/mb-example1-admin.ts b/ts/client/src/scripts/mb-example1-admin.ts index c80f204af..887d37a02 100644 --- a/ts/client/src/scripts/mb-example1-admin.ts +++ b/ts/client/src/scripts/mb-example1-admin.ts @@ -337,10 +337,7 @@ async function createUser(userKeypair: string) { const user = result[2]; console.log(`Creating MangoAccount...`); - const mangoAccount = await client.getOrCreateMangoAccount( - group, - user.publicKey, - ); + const mangoAccount = await client.getOrCreateMangoAccount(group); if (!mangoAccount) { throw new Error(`MangoAccount not found for user ${user.publicKey}`); } diff --git a/ts/client/src/scripts/mb-example1-user.ts b/ts/client/src/scripts/mb-example1-user.ts index 9aa248a20..8cb46c95f 100644 --- a/ts/client/src/scripts/mb-example1-user.ts +++ b/ts/client/src/scripts/mb-example1-user.ts @@ -34,10 +34,7 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); - const mangoAccount = await client.getOrCreateMangoAccount( - group, - user.publicKey, - ); + const mangoAccount = await client.getOrCreateMangoAccount(group); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(mangoAccount.toString(group)); From a2f4cef940151f2e138a054776864ae4eab76a66 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Fri, 30 Sep 2022 13:33:21 +0200 Subject: [PATCH 29/39] mc/ts derive and not hardcode size (#257) * ts: simplify getOrCreateMangoAccount Signed-off-by: microwavedcola1 * ts: derive and not hardcode sizes for perp accounts when creating Signed-off-by: microwavedcola1 * ts: fix script Signed-off-by: microwavedcola1 * ts: remove todo, not relevant anymore Signed-off-by: microwavedcola1 * Fixes from review Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- ts/client/src/accounts/mangoAccount.ts | 2 +- ts/client/src/client.ts | 21 ++++++++----- ts/client/src/debug-scripts/mb-debug-user.ts | 31 ++++++-------------- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 1bc777101..9ef7b979b 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -272,7 +272,7 @@ export class MangoAccount { } /** - * Sum of all the assets i.e. token deposits, borrows, total assets in spot open orders, (perps positions is todo) in terms of quote value. + * Sum of all the assets i.e. token deposits, borrows, total assets in spot open orders, and perps positions. * @returns equity, in native quote */ getEquity(group: Group): I80F48 { diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 619c39dfa..de4fc78a9 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1351,6 +1351,13 @@ export class MangoClient { const asks = new Keypair(); const eventQueue = new Keypair(); + const bookSideSize = (this.program as any)._coder.accounts.size( + (this.program.account.bookSide as any)._idlAccount, + ); + const eventQueueSize = (this.program as any)._coder.accounts.size( + (this.program.account.eventQueue as any)._idlAccount, + ); + return await this.program.methods .perpCreateMarket( perpMarketIndex, @@ -1390,15 +1397,13 @@ export class MangoClient { payer: (this.program.provider as AnchorProvider).wallet.publicKey, }) .preInstructions([ - // TODO: try to pick up sizes of bookside and eventqueue from IDL, so we can stay in sync with program - // book sides SystemProgram.createAccount({ programId: this.program.programId, - space: 8 + 98584, + space: bookSideSize, lamports: await this.program.provider.connection.getMinimumBalanceForRentExemption( - 8 + 98584, + bookSideSize, ), fromPubkey: (this.program.provider as AnchorProvider).wallet .publicKey, @@ -1406,10 +1411,10 @@ export class MangoClient { }), SystemProgram.createAccount({ programId: this.program.programId, - space: 8 + 98584, + space: bookSideSize, lamports: await this.program.provider.connection.getMinimumBalanceForRentExemption( - 8 + 98584, + bookSideSize, ), fromPubkey: (this.program.provider as AnchorProvider).wallet .publicKey, @@ -1418,10 +1423,10 @@ export class MangoClient { // event queue SystemProgram.createAccount({ programId: this.program.programId, - space: 8 + 4 * 2 + 8 + 488 * 208, + space: eventQueueSize, lamports: await this.program.provider.connection.getMinimumBalanceForRentExemption( - 8 + 4 * 2 + 8 + 488 * 208, + eventQueueSize, ), fromPubkey: (this.program.provider as AnchorProvider).wallet .publicKey, diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index 7f315ef66..cc3385795 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -29,21 +29,6 @@ async function debugUser( await mangoAccount.reload(client); - console.log( - 'buildFixedAccountRetrieverHealthAccounts ' + - client - .buildFixedAccountRetrieverHealthAccounts( - group, - mangoAccount, - [ - group.banksMapByName.get('BTC')![0], - group.banksMapByName.get('USDC')![0], - ], - [], - ) - .map((pk) => pk.toBase58()) - .join(', '), - ); console.log( 'mangoAccount.getEquity() ' + toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()), @@ -146,23 +131,25 @@ async function debugUser( function getMaxForPerpWrapper(perpMarket: PerpMarket) { console.log( - `getMaxQuoteForPerpBidUi ${perpMarket.name} ` + + `getMaxQuoteForPerpBidUi ${perpMarket.perpMarketIndex} ` + mangoAccount.getMaxQuoteForPerpBidUi( group, - perpMarket.name, - perpMarket.price, + perpMarket.perpMarketIndex, + perpMarket.price.toNumber(), ), ); console.log( - `getMaxBaseForPerpAskUi ${perpMarket.name} ` + + `getMaxBaseForPerpAskUi ${perpMarket.perpMarketIndex} ` + mangoAccount.getMaxBaseForPerpAskUi( group, - perpMarket.name, - perpMarket.price, + perpMarket.perpMarketIndex, + perpMarket.price.toNumber(), ), ); } - for (const perpMarket of Array.from(group.perpMarketsMap.values())) { + for (const perpMarket of Array.from( + group.perpMarketsMapByMarketIndex.values(), + )) { getMaxForPerpWrapper(perpMarket); } From d1079bb1b948cb0bf05a3af623f19ffb2354dada Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Fri, 30 Sep 2022 14:59:12 +0200 Subject: [PATCH 30/39] Mc/ts increase error tolerance for binary search (#258) * ts: a higher error tolerance is sufficient Signed-off-by: microwavedcola1 * Fixes from review Signed-off-by: microwavedcola1 * program: fix error size Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- programs/mango-v4/src/state/health.rs | 4 ++-- ts/client/src/accounts/healthCache.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 19e9c52cf..a3a557406 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -1158,7 +1158,7 @@ fn binary_search( fun: impl Fn(I80F48) -> I80F48, ) -> Result { let max_iterations = 20; - let target_error = I80F48!(0.01); + let target_error = I80F48!(0.1); require_msg!( (left_value - target_value).signum() * (right_value - target_value).signum() != I80F48::ONE, "internal error: left {} and right {} don't contain the target value {}", @@ -2096,7 +2096,7 @@ mod tests { println!( "checking for price_factor: {price_factor}, target ratio {ratio}: actual ratio: {actual_ratio}, plus ratio: {plus_ratio}, base_lots: {base_lots}", ); - let max_binary_search_error = 0.01; + let max_binary_search_error = 0.1; assert!(ratio <= actual_ratio); assert!(plus_ratio - max_binary_search_error <= ratio); }; diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index b06477ab7..b312c5f50 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -431,8 +431,7 @@ export class HealthCache { healthRatioAfterActionFn: (I80F48) => I80F48, ): I80F48 { const maxIterations = 40; - // TODO: make relative to health ratio decimals? Might be over engineering - const targetError = I80F48.fromNumber(0.001); + const targetError = I80F48.fromNumber(0.1); if ( (leftRatio.sub(targetRatio).isPos() && From bafaf737459e899f5a53cce77eb8573687c0921d Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Fri, 30 Sep 2022 15:07:43 +0200 Subject: [PATCH 31/39] Mc/ts numbers - cleanup usage of all numbers (#259) * ts: a higher error tolerance is sufficient Signed-off-by: microwavedcola1 * ts: move stuff around Signed-off-by: microwavedcola1 * ts: string representation while printing Signed-off-by: microwavedcola1 * ts: number cleanup Signed-off-by: microwavedcola1 * ts: fix tsc errors Signed-off-by: microwavedcola1 * ts: cleanup creation of I80F48 from BN Signed-off-by: microwavedcola1 * ts: fixes from review Signed-off-by: microwavedcola1 * ts: fixed from review Signed-off-by: microwavedcola1 * revert Signed-off-by: microwavedcola1 * ts: fix from call Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- ts/client/src/accounts/bank.ts | 58 +++--- ts/client/src/accounts/group.ts | 73 +++----- ts/client/src/accounts/healthCache.spec.ts | 24 +-- ts/client/src/accounts/healthCache.ts | 54 +++--- ts/client/src/accounts/mangoAccount.ts | 102 +++++------ ts/client/src/accounts/oracle.ts | 6 +- ts/client/src/accounts/perp.ts | 36 ++-- ts/client/src/accounts/serum3.ts | 2 +- ts/client/src/client.ts | 21 +-- ts/client/src/development.ts | 76 ++++++++ ts/client/src/index.ts | 2 +- ts/client/src/{accounts => numbers}/I80F48.ts | 2 +- ts/client/src/numbers/numbers.spec.ts | 37 ++++ ts/client/src/utils.ts | 165 +++++------------- 14 files changed, 321 insertions(+), 337 deletions(-) create mode 100644 ts/client/src/development.ts rename ts/client/src/{accounts => numbers}/I80F48.ts (99%) create mode 100644 ts/client/src/numbers/numbers.spec.ts diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index 0bd410447..73f239bac 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -1,8 +1,8 @@ import { BN } from '@project-serum/anchor'; import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { PublicKey } from '@solana/web3.js'; -import { As, nativeI80F48ToUi } from '../utils'; -import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48'; +import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48'; +import { As, toUiDecimals } from '../utils'; export const QUOTE_DECIMALS = 6; @@ -209,61 +209,61 @@ export class Bank implements BankForHealth { '\n oracle - ' + this.oracle.toBase58() + '\n price - ' + - this._price?.toNumber() + + this._price?.toString() + '\n uiPrice - ' + this._uiPrice + '\n deposit index - ' + - this.depositIndex.toNumber() + + this.depositIndex.toString() + '\n borrow index - ' + - this.borrowIndex.toNumber() + + this.borrowIndex.toString() + '\n indexedDeposits - ' + - this.indexedDeposits.toNumber() + + this.indexedDeposits.toString() + '\n indexedBorrows - ' + - this.indexedBorrows.toNumber() + + this.indexedBorrows.toString() + '\n cachedIndexedTotalDeposits - ' + - this.cachedIndexedTotalDeposits.toNumber() + + this.cachedIndexedTotalDeposits.toString() + '\n cachedIndexedTotalBorrows - ' + - this.cachedIndexedTotalBorrows.toNumber() + + this.cachedIndexedTotalBorrows.toString() + '\n indexLastUpdated - ' + new Date(this.indexLastUpdated.toNumber() * 1000) + '\n bankRateLastUpdated - ' + new Date(this.bankRateLastUpdated.toNumber() * 1000) + '\n avgUtilization - ' + - this.avgUtilization.toNumber() + + this.avgUtilization.toString() + '\n adjustmentFactor - ' + - this.adjustmentFactor.toNumber() + + this.adjustmentFactor.toString() + '\n maxRate - ' + - this.maxRate.toNumber() + + this.maxRate.toString() + '\n util0 - ' + - this.util0.toNumber() + + this.util0.toString() + '\n rate0 - ' + - this.rate0.toNumber() + + this.rate0.toString() + '\n util1 - ' + - this.util1.toNumber() + + this.util1.toString() + '\n rate1 - ' + - this.rate1.toNumber() + + this.rate1.toString() + '\n loanFeeRate - ' + - this.loanFeeRate.toNumber() + + this.loanFeeRate.toString() + '\n loanOriginationFeeRate - ' + - this.loanOriginationFeeRate.toNumber() + + this.loanOriginationFeeRate.toString() + '\n maintAssetWeight - ' + - this.maintAssetWeight.toNumber() + + this.maintAssetWeight.toString() + '\n initAssetWeight - ' + - this.initAssetWeight.toNumber() + + this.initAssetWeight.toString() + '\n maintLiabWeight - ' + - this.maintLiabWeight.toNumber() + + this.maintLiabWeight.toString() + '\n initLiabWeight - ' + - this.initLiabWeight.toNumber() + + this.initLiabWeight.toString() + '\n liquidationFee - ' + - this.liquidationFee.toNumber() + + this.liquidationFee.toString() + '\n uiDeposits() - ' + this.uiDeposits() + '\n uiBorrows() - ' + this.uiBorrows() + '\n getDepositRate() - ' + - this.getDepositRate().toNumber() + + this.getDepositRate().toString() + '\n getBorrowRate() - ' + - this.getBorrowRate().toNumber() + this.getBorrowRate().toString() ); } @@ -294,17 +294,17 @@ export class Bank implements BankForHealth { } uiDeposits(): number { - return nativeI80F48ToUi( + return toUiDecimals( this.indexedDeposits.mul(this.depositIndex), this.mintDecimals, - ).toNumber(); + ); } uiBorrows(): number { - return nativeI80F48ToUi( + return toUiDecimals( this.indexedBorrows.mul(this.borrowIndex), this.mintDecimals, - ).toNumber(); + ); } /** diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 9f746a461..86e757b18 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -16,9 +16,9 @@ import BN from 'bn.js'; import { MangoClient } from '../client'; import { SERUM3_PROGRAM_ID } from '../constants'; import { Id } from '../ids'; -import { toNativeDecimals, toUiDecimals } from '../utils'; +import { I80F48, ONE_I80F48 } from '../numbers/I80F48'; +import { toNative, toNativeI80F48, toUiDecimals } from '../utils'; import { Bank, MintInfo, TokenIndex } from './bank'; -import { I80F48, ONE_I80F48 } from './I80F48'; import { isPythOracle, isSwitchboardOracle, @@ -92,7 +92,7 @@ export class Group { public perpMarketsMapByName: Map, public mintInfosMapByTokenIndex: Map, public mintInfosMapByMint: Map, - public vaultAmountsMap: Map, + public vaultAmountsMap: Map, ) {} public async reloadAll(client: MangoClient): Promise { @@ -382,9 +382,10 @@ export class Group { if (!vaultAi) { throw new Error(`Undefined vaultAi for ${vaultPks[i]}`!); } - const vaultAmount = coder() - .accounts.decode('token', vaultAi.data) - .amount.toNumber(); + const vaultAmount = coder().accounts.decode( + 'token', + vaultAi.data, + ).amount; return [vaultPks[i].toBase58(), vaultAmount]; }), ); @@ -412,34 +413,28 @@ export class Group { return banks[0]; } - /** - * - * @param mintPk - * @returns sum of native balances of vaults for all banks for a token (fetched from vaultAmountsMap cache) - */ - public getTokenVaultBalanceByMint(mintPk: PublicKey): I80F48 { - const banks = this.banksMapByMint.get(mintPk.toBase58()); - if (!banks) throw new Error(`No bank found for mint ${mintPk}!`); - let totalAmount = 0; - for (const bank of banks) { - const amount = this.vaultAmountsMap.get(bank.vault.toBase58()); - if (amount) { - totalAmount += amount; - } - } - return I80F48.fromNumber(totalAmount); - } - /** * * @param mintPk * @returns sum of ui balances of vaults for all banks for a token */ public getTokenVaultBalanceByMintUi(mintPk: PublicKey): number { - const vaultBalance = this.getTokenVaultBalanceByMint(mintPk); - const mintDecimals = this.getMintDecimals(mintPk); + const banks = this.banksMapByMint.get(mintPk.toBase58()); + if (!banks) { + throw new Error(`No bank found for mint ${mintPk}!`); + } + const totalAmount = new BN(0); + for (const bank of banks) { + const amount = this.vaultAmountsMap.get(bank.vault.toBase58()); + if (!amount) { + throw new Error( + `Vault balance not found for bank ${bank.name} ${bank.bankNum}!`, + ); + } + totalAmount.iadd(amount); + } - return toUiDecimals(vaultBalance, mintDecimals); + return toUiDecimals(totalAmount, this.getMintDecimals(mintPk)); } public getSerum3MarketByMarketIndex(marketIndex: MarketIndex): Serum3Market { @@ -575,31 +570,21 @@ export class Group { } public toUiPrice(price: I80F48, baseDecimals: number): number { - return price - .mul( - I80F48.fromNumber( - Math.pow(10, baseDecimals - this.getInsuranceMintDecimals()), - ), - ) - .toNumber(); + return toUiDecimals(price, baseDecimals - this.getInsuranceMintDecimals()); } public toNativePrice(uiPrice: number, baseDecimals: number): I80F48 { - return I80F48.fromNumber(uiPrice).mul( - I80F48.fromNumber( - Math.pow( - 10, - // note: our oracles are quoted in USD and our insurance mint is USD - // please update when these assumptions change - this.getInsuranceMintDecimals() - baseDecimals, - ), - ), + return toNativeI80F48( + uiPrice, + // note: our oracles are quoted in USD and our insurance mint is USD + // please update when these assumptions change + this.getInsuranceMintDecimals() - baseDecimals, ); } public toNativeDecimals(uiAmount: number, mintPk: PublicKey): BN { const decimals = this.getMintDecimals(mintPk); - return toNativeDecimals(uiAmount, decimals); + return toNative(uiAmount, decimals); } toString(): string { diff --git a/ts/client/src/accounts/healthCache.spec.ts b/ts/client/src/accounts/healthCache.spec.ts index e0beb5252..22ad523f3 100644 --- a/ts/client/src/accounts/healthCache.spec.ts +++ b/ts/client/src/accounts/healthCache.spec.ts @@ -1,10 +1,10 @@ import { BN } from '@project-serum/anchor'; import { OpenOrders } from '@project-serum/serum'; import { expect } from 'chai'; +import { I80F48, ZERO_I80F48 } from '../numbers/I80F48'; import { toUiDecimalsForQuote } from '../utils'; import { BankForHealth, TokenIndex } from './bank'; import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache'; -import { I80F48, ZERO_I80F48 } from './I80F48'; import { HealthType, PerpPosition } from './mangoAccount'; import { PerpMarket } from './perp'; import { MarketIndex } from './serum3'; @@ -81,12 +81,12 @@ describe('Health Cache', () => { const pM = mockPerpMarket(9, 0.1, 0.2, targetBank.price); const pp = new PerpPosition( pM.perpMarketIndex, - 3, + new BN(3), I80F48.fromNumber(-310), - 7, - 11, - 1, - 2, + new BN(7), + new BN(11), + new BN(1), + new BN(2), I80F48.fromNumber(0), I80F48.fromNumber(0), ); @@ -182,12 +182,12 @@ describe('Health Cache', () => { const pM = mockPerpMarket(9, 0.1, 0.2, bank2.price); const pp = new PerpPosition( pM.perpMarketIndex, - fixture.perp1[0], + new BN(fixture.perp1[0]), I80F48.fromNumber(fixture.perp1[1]), - fixture.perp1[2], - fixture.perp1[3], - 0, - 0, + new BN(fixture.perp1[2]), + new BN(fixture.perp1[3]), + new BN(0), + new BN(0), I80F48.fromNumber(0), I80F48.fromNumber(0), ); @@ -430,6 +430,6 @@ describe('Health Cache', () => { I80F48.fromNumber(0.95), ), ).toFixed(3), - ).equals('90.477'); + ).equals('90.176'); }); }); diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index b312c5f50..e3dc93af8 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -2,15 +2,15 @@ import { BN } from '@project-serum/anchor'; import { OpenOrders } from '@project-serum/serum'; import { PublicKey } from '@solana/web3.js'; import _ from 'lodash'; -import { Bank, BankForHealth, TokenIndex } from './bank'; -import { Group } from './group'; import { HUNDRED_I80F48, I80F48, I80F48Dto, MAX_I80F48, ZERO_I80F48, -} from './I80F48'; +} from '../numbers/I80F48'; +import { Bank, BankForHealth, TokenIndex } from './bank'; +import { Group } from './group'; import { HealthType, MangoAccount, PerpPosition } from './mangoAccount'; import { PerpMarket, PerpOrderSide } from './perp'; @@ -735,7 +735,7 @@ export class HealthCache { const perpInfoIndex = this.getOrCreatePerpInfoIndex(perpMarket); const perpInfo = this.perpInfos[perpInfoIndex]; const oraclePrice = perpInfo.oraclePrice; - const baseLotSize = I80F48.fromString(perpMarket.baseLotSize.toString()); + const baseLotSize = I80F48.fromI64(perpMarket.baseLotSize); // If the price is sufficiently good then health will just increase from trading const finalHealthSlope = @@ -940,21 +940,21 @@ export class Serum3Info { oo: OpenOrders, ): Serum3Info { // add the amounts that are freely settleable - const baseFree = I80F48.fromString(oo.baseTokenFree.toString()); + const baseFree = I80F48.fromI64(oo.baseTokenFree); // NOTE: referrerRebatesAccrued is not declared on oo class, but the layout // is aware of it - const quoteFree = I80F48.fromString( - oo.quoteTokenFree.add((oo as any).referrerRebatesAccrued).toString(), + const quoteFree = I80F48.fromI64( + oo.quoteTokenFree.add((oo as any).referrerRebatesAccrued), ); baseInfo.balance.iadd(baseFree.mul(baseInfo.oraclePrice)); quoteInfo.balance.iadd(quoteFree.mul(quoteInfo.oraclePrice)); // add the reserved amount to both sides, to have the worst-case covered - const reservedBase = I80F48.fromString( - oo.baseTokenTotal.sub(oo.baseTokenFree).toString(), + const reservedBase = I80F48.fromI64( + oo.baseTokenTotal.sub(oo.baseTokenFree), ); - const reservedQuote = I80F48.fromString( - oo.quoteTokenTotal.sub(oo.quoteTokenFree).toString(), + const reservedQuote = I80F48.fromI64( + oo.quoteTokenTotal.sub(oo.quoteTokenFree), ); const reservedBalance = reservedBase .mul(baseInfo.oraclePrice) @@ -1059,21 +1059,17 @@ export class PerpInfo { perpMarket: PerpMarket, perpPosition: PerpPosition, ): PerpInfo { - const baseLotSize = I80F48.fromString(perpMarket.baseLotSize.toString()); - const baseLots = I80F48.fromNumber( - perpPosition.basePositionLots + perpPosition.takerBaseLots, + const baseLotSize = I80F48.fromI64(perpMarket.baseLotSize); + const baseLots = I80F48.fromI64( + perpPosition.basePositionLots.add(perpPosition.takerBaseLots), ); const unsettledFunding = perpPosition.unsettledFunding(perpMarket); - const takerQuote = I80F48.fromString( - new BN(perpPosition.takerQuoteLots) - .mul(perpMarket.quoteLotSize) - .toString(), + const takerQuote = I80F48.fromI64( + new BN(perpPosition.takerQuoteLots).mul(perpMarket.quoteLotSize), ); - const quoteCurrent = I80F48.fromString( - perpPosition.quotePositionNative.toString(), - ) + const quoteCurrent = perpPosition.quotePositionNative .sub(unsettledFunding) .add(takerQuote); @@ -1118,26 +1114,18 @@ export class PerpInfo { // scenario1 < scenario2 // iff abs(bidsNetLots) > abs(asksNetLots) - const bidsNetLots = baseLots.add( - I80F48.fromNumber(perpPosition.bidsBaseLots), - ); - const asksNetLots = baseLots.sub( - I80F48.fromNumber(perpPosition.asksBaseLots), - ); + const bidsNetLots = baseLots.add(I80F48.fromI64(perpPosition.bidsBaseLots)); + const asksNetLots = baseLots.sub(I80F48.fromI64(perpPosition.asksBaseLots)); const lotsToQuote = baseLotSize.mul(perpMarket.price); let base, quote; if (bidsNetLots.abs().gt(asksNetLots.abs())) { - const bidsBaseLots = I80F48.fromString( - perpPosition.bidsBaseLots.toString(), - ); + const bidsBaseLots = I80F48.fromI64(perpPosition.bidsBaseLots); base = bidsNetLots.mul(lotsToQuote); quote = quoteCurrent.sub(bidsBaseLots.mul(lotsToQuote)); } else { - const asksBaseLots = I80F48.fromString( - perpPosition.asksBaseLots.toString(), - ); + const asksBaseLots = I80F48.fromI64(perpPosition.asksBaseLots); base = asksNetLots.mul(lotsToQuote); quote = quoteCurrent.add(asksBaseLots.mul(lotsToQuote)); } diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 9ef7b979b..f38d0eded 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -4,16 +4,11 @@ import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market'; import { PublicKey } from '@solana/web3.js'; import { MangoClient } from '../client'; import { SERUM3_PROGRAM_ID } from '../constants'; -import { - nativeI80F48ToUi, - toNative, - toUiDecimals, - toUiDecimalsForQuote, -} from '../utils'; +import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48'; +import { toNativeI80F48, toUiDecimals, toUiDecimalsForQuote } from '../utils'; import { Bank, TokenIndex } from './bank'; import { Group } from './group'; import { HealthCache } from './healthCache'; -import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48'; import { PerpMarket, PerpMarketIndex, PerpOrder, PerpOrderSide } from './perp'; import { MarketIndex, Serum3Side } from './serum3'; export class MangoAccount { @@ -262,13 +257,9 @@ export class MangoAccount { * @param healthType * @returns health ratio, in percentage form, capped to 100 */ - getHealthRatioUi(group: Group, healthType: HealthType): number | undefined { + getHealthRatioUi(group: Group, healthType: HealthType): number { const ratio = this.getHealthRatio(group, healthType).toNumber(); - if (ratio) { - return ratio > 100 ? 100 : Math.trunc(ratio); - } else { - return undefined; - } + return ratio > 100 ? 100 : Math.trunc(ratio); } /** @@ -287,19 +278,15 @@ export class MangoAccount { const baseBank = group.getFirstBankByTokenIndex(sp.baseTokenIndex); tokensMap .get(baseBank.tokenIndex)! - .iadd( - I80F48.fromString(oo.baseTokenTotal.toString()).mul(baseBank.price), - ); + .iadd(I80F48.fromI64(oo.baseTokenTotal).mul(baseBank.price)); const quoteBank = group.getFirstBankByTokenIndex(sp.quoteTokenIndex); // NOTE: referrerRebatesAccrued is not declared on oo class, but the layout // is aware of it tokensMap .get(baseBank.tokenIndex)! .iadd( - I80F48.fromString( - oo.quoteTokenTotal - .add((oo as any).referrerRebatesAccrued) - .toString(), + I80F48.fromI64( + oo.quoteTokenTotal.add((oo as any).referrerRebatesAccrued), ).mul(quoteBank.price), ); } @@ -477,7 +464,7 @@ export class MangoAccount { ): number | undefined { const nativeTokenChanges = uiTokenChanges.map((tokenChange) => { return { - nativeTokenAmount: toNative( + nativeTokenAmount: toNativeI80F48( tokenChange.uiTokenAmount, group.getMintDecimals(tokenChange.mintPk), ), @@ -634,7 +621,7 @@ export class MangoAccount { .simHealthRatioWithSerum3BidChanges( baseBank, quoteBank, - toNative( + toNativeI80F48( uiQuoteAmount, group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) .mintDecimals, @@ -672,7 +659,7 @@ export class MangoAccount { .simHealthRatioWithSerum3AskChanges( baseBank, quoteBank, - toNative( + toNativeI80F48( uiBaseAmount, group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) .mintDecimals, @@ -703,11 +690,9 @@ export class MangoAccount { I80F48.fromNumber(2), group.toNativePrice(uiPrice, perpMarket.baseDecimals), ); - const nativeBase = baseLots.mul( - I80F48.fromString(perpMarket.baseLotSize.toString()), - ); + const nativeBase = baseLots.mul(I80F48.fromI64(perpMarket.baseLotSize)); const nativeQuote = nativeBase.mul(perpMarket.price); - return toUiDecimalsForQuote(nativeQuote.toNumber()); + return toUiDecimalsForQuote(nativeQuote); } /** @@ -856,7 +841,7 @@ export class TokenPosition { * @returns UI balance, is signed */ public balanceUi(bank: Bank): number { - return nativeI80F48ToUi(this.balance(bank), bank.mintDecimals).toNumber(); + return toUiDecimals(this.balance(bank), bank.mintDecimals); } /** @@ -864,7 +849,7 @@ export class TokenPosition { * @returns UI deposits, 0 if position has borrows */ public depositsUi(bank: Bank): number { - return nativeI80F48ToUi(this.deposits(bank), bank.mintDecimals).toNumber(); + return toUiDecimals(this.deposits(bank), bank.mintDecimals); } /** @@ -872,7 +857,7 @@ export class TokenPosition { * @returns UI borrows, 0 if position has deposits */ public borrowsUi(bank: Bank): number { - return nativeI80F48ToUi(this.borrows(bank), bank.mintDecimals).toNumber(); + return toUiDecimals(this.borrows(bank), bank.mintDecimals); } public toString(group?: Group, index?: number): string { @@ -947,12 +932,12 @@ export class PerpPosition { static from(dto: PerpPositionDto): PerpPosition { return new PerpPosition( dto.marketIndex as PerpMarketIndex, - dto.basePositionLots.toNumber(), + dto.basePositionLots, I80F48.from(dto.quotePositionNative), - dto.bidsBaseLots.toNumber(), - dto.asksBaseLots.toNumber(), - dto.takerBaseLots.toNumber(), - dto.takerQuoteLots.toNumber(), + dto.bidsBaseLots, + dto.asksBaseLots, + dto.takerBaseLots, + dto.takerQuoteLots, I80F48.from(dto.longSettledFunding), I80F48.from(dto.shortSettledFunding), ); @@ -960,12 +945,12 @@ export class PerpPosition { constructor( public marketIndex: PerpMarketIndex, - public basePositionLots: number, + public basePositionLots: BN, public quotePositionNative: I80F48, - public bidsBaseLots: number, - public asksBaseLots: number, - public takerBaseLots: number, - public takerQuoteLots: number, + public bidsBaseLots: BN, + public asksBaseLots: BN, + public takerBaseLots: BN, + public takerQuoteLots: BN, public longSettledFunding: I80F48, public shortSettledFunding: I80F48, ) {} @@ -975,32 +960,32 @@ export class PerpPosition { } public unsettledFunding(perpMarket: PerpMarket): I80F48 { - if (this.basePositionLots > 0) { + if (this.basePositionLots.gt(new BN(0))) { return perpMarket.longFunding .sub(this.longSettledFunding) - .mul(I80F48.fromString(this.basePositionLots.toString())); - } else if (this.basePositionLots < 0) { + .mul(I80F48.fromI64(this.basePositionLots)); + } else if (this.basePositionLots.lt(new BN(0))) { return perpMarket.shortFunding .sub(this.shortSettledFunding) - .mul(I80F48.fromString(this.basePositionLots.toString())); + .mul(I80F48.fromI64(this.basePositionLots)); } return ZERO_I80F48(); } public getEquity(perpMarket: PerpMarket): I80F48 { - const lotsToQuote = I80F48.fromString( - perpMarket.baseLotSize.toString(), - ).mul(perpMarket.price); + const lotsToQuote = I80F48.fromI64(perpMarket.baseLotSize).mul( + perpMarket.price, + ); - const baseLots = I80F48.fromNumber( - this.basePositionLots + this.takerBaseLots, + const baseLots = I80F48.fromI64( + this.basePositionLots.add(this.takerBaseLots), ); const unsettledFunding = this.unsettledFunding(perpMarket); - const takerQuote = I80F48.fromString( - new BN(this.takerQuoteLots).mul(perpMarket.quoteLotSize).toString(), + const takerQuote = I80F48.fromI64( + new BN(this.takerQuoteLots).mul(perpMarket.quoteLotSize), ); - const quoteCurrent = I80F48.fromString(this.quotePositionNative.toString()) + const quoteCurrent = this.quotePositionNative .sub(unsettledFunding) .add(takerQuote); @@ -1008,11 +993,12 @@ export class PerpPosition { } public hasOpenOrders(): boolean { + const zero = new BN(0); return ( - this.asksBaseLots != 0 || - this.bidsBaseLots != 0 || - this.takerBaseLots != 0 || - this.takerQuoteLots != 0 + !this.asksBaseLots.eq(zero) || + !this.bidsBaseLots.eq(zero) || + !this.takerBaseLots.eq(zero) || + !this.takerQuoteLots.eq(zero) ); } } @@ -1038,7 +1024,7 @@ export class PerpOo { return new PerpOo( dto.orderSide, dto.orderMarket, - dto.clientOrderId.toNumber(), + dto.clientOrderId, dto.orderId, ); } @@ -1046,7 +1032,7 @@ export class PerpOo { constructor( public orderSide: any, public orderMarket: 0, - public clientOrderId: number, + public clientOrderId: BN, public orderId: BN, ) {} } diff --git a/ts/client/src/accounts/oracle.ts b/ts/client/src/accounts/oracle.ts index 10420caea..0117a04c3 100644 --- a/ts/client/src/accounts/oracle.ts +++ b/ts/client/src/accounts/oracle.ts @@ -7,7 +7,7 @@ import { SwitchboardDecimal, } from '@switchboard-xyz/switchboard-v2'; import BN from 'bn.js'; -import { I80F48, I80F48Dto } from './I80F48'; +import { I80F48, I80F48Dto } from '../numbers/I80F48'; const SBV1_DEVNET_PID = new PublicKey( '7azgmy1pFXHikv36q1zZASvFq5vFa39TT9NweVugKKTU', @@ -20,7 +20,7 @@ let sbv2MainnetProgram; export class StubOracle { public price: I80F48; - public lastUpdated: number; + public lastUpdated: BN; static from( publicKey: PublicKey, @@ -48,7 +48,7 @@ export class StubOracle { lastUpdated: BN, ) { this.price = I80F48.from(price); - this.lastUpdated = lastUpdated.toNumber(); + this.lastUpdated = lastUpdated; } } diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index 5bf8fa2d8..986b79918 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -3,9 +3,9 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { PublicKey } from '@solana/web3.js'; import Big from 'big.js'; import { MangoClient } from '../client'; -import { As, U64_MAX_BN } from '../utils'; +import { I80F48, I80F48Dto } from '../numbers/I80F48'; +import { As, toNative, U64_MAX_BN } from '../utils'; import { OracleConfig, QUOTE_DECIMALS } from './bank'; -import { I80F48, I80F48Dto } from './I80F48'; export type PerpMarketIndex = number & As<'perp-market-index'>; @@ -22,8 +22,8 @@ export class PerpMarket { public maxFunding: I80F48; public longFunding: I80F48; public shortFunding: I80F48; - public openInterest: number; - public seqNum: number; + public openInterest: BN; + public seqNum: BN; public feesAccrued: I80F48; priceLotsToUiConverter: number; baseLotsToUiConverter: number; @@ -146,8 +146,8 @@ export class PerpMarket { this.maxFunding = I80F48.from(maxFunding); this.longFunding = I80F48.from(longFunding); this.shortFunding = I80F48.from(shortFunding); - this.openInterest = openInterest.toNumber(); - this.seqNum = seqNum.toNumber(); + this.openInterest = openInterest; + this.seqNum = seqNum; this.feesAccrued = I80F48.from(feesAccrued); this.priceLotsToUiConverter = new Big(10) @@ -241,21 +241,17 @@ export class PerpMarket { } public uiPriceToLots(price: number): BN { - return new BN(price * Math.pow(10, QUOTE_DECIMALS)) + return toNative(price, QUOTE_DECIMALS) .mul(this.baseLotSize) .div(this.quoteLotSize.mul(new BN(Math.pow(10, this.baseDecimals)))); } public uiBaseToLots(quantity: number): BN { - return new BN(quantity * Math.pow(10, this.baseDecimals)).div( - this.baseLotSize, - ); + return toNative(quantity, this.baseDecimals).div(this.baseLotSize); } public uiQuoteToLots(uiQuote: number): BN { - return new BN(uiQuote * Math.pow(10, QUOTE_DECIMALS)).div( - this.quoteLotSize, - ); + return toNative(uiQuote, QUOTE_DECIMALS).div(this.quoteLotSize); } public priceLotsToUi(price: BN): number { @@ -276,19 +272,19 @@ export class PerpMarket { '\n perpMarketIndex -' + this.perpMarketIndex + '\n maintAssetWeight -' + - this.maintAssetWeight.toNumber() + + this.maintAssetWeight.toString() + '\n initAssetWeight -' + - this.initAssetWeight.toNumber() + + this.initAssetWeight.toString() + '\n maintLiabWeight -' + - this.maintLiabWeight.toNumber() + + this.maintLiabWeight.toString() + '\n initLiabWeight -' + - this.initLiabWeight.toNumber() + + this.initLiabWeight.toString() + '\n liquidationFee -' + - this.liquidationFee.toNumber() + + this.liquidationFee.toString() + '\n makerFee -' + - this.makerFee.toNumber() + + this.makerFee.toString() + '\n takerFee -' + - this.takerFee.toNumber() + this.takerFee.toString() ); } } diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index 56324e064..38cbc83eb 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -7,7 +7,7 @@ import { SERUM3_PROGRAM_ID } from '../constants'; import { As } from '../utils'; import { TokenIndex } from './bank'; import { Group } from './group'; -import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48'; +import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48'; export type MarketIndex = number & As<'market-index'>; diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index de4fc78a9..2d5e762a2 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -26,7 +26,6 @@ import { import bs58 from 'bs58'; import { Bank, MintInfo, TokenIndex } from './accounts/bank'; import { Group } from './accounts/group'; -import { I80F48 } from './accounts/I80F48'; import { MangoAccount, PerpPosition, @@ -52,12 +51,13 @@ import { import { SERUM3_PROGRAM_ID } from './constants'; import { Id } from './ids'; import { IDL, MangoV4 } from './mango_v4'; +import { I80F48 } from './numbers/I80F48'; import { FlashLoanType, InterestRateParams } from './types'; import { createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddress, I64_MAX_BN, - toNativeDecimals, + toNative, } from './utils'; import { sendTransaction } from './utils/rpc'; @@ -741,7 +741,7 @@ export class MangoClient { amount: number, ): Promise { const decimals = group.getMintDecimals(mintPk); - const nativeAmount = toNativeDecimals(amount, decimals).toNumber(); + const nativeAmount = toNative(amount, decimals); return await this.tokenDepositNative( group, mangoAccount, @@ -754,7 +754,7 @@ export class MangoClient { group: Group, mangoAccount: MangoAccount, mintPk: PublicKey, - nativeAmount: number, + nativeAmount: BN, ): Promise { const bank = group.getFirstBankByMint(mintPk); @@ -769,13 +769,13 @@ export class MangoClient { const additionalSigners: Signer[] = []; if (mintPk.equals(WRAPPED_SOL_MINT)) { wrappedSolAccount = new Keypair(); - const lamports = nativeAmount + 1e7; + const lamports = nativeAmount.add(new BN(1e7)); preInstructions = [ SystemProgram.createAccount({ fromPubkey: mangoAccount.owner, newAccountPubkey: wrappedSolAccount.publicKey, - lamports, + lamports: lamports.toNumber(), space: 165, programId: TOKEN_PROGRAM_ID, }), @@ -841,10 +841,7 @@ export class MangoClient { amount: number, allowBorrow: boolean, ): Promise { - const nativeAmount = toNativeDecimals( - amount, - group.getMintDecimals(mintPk), - ).toNumber(); + const nativeAmount = toNative(amount, group.getMintDecimals(mintPk)); return await this.tokenWithdrawNative( group, mangoAccount, @@ -858,7 +855,7 @@ export class MangoClient { group: Group, mangoAccount: MangoAccount, mintPk: PublicKey, - nativeAmount: number, + nativeAmount: BN, allowBorrow: boolean, ): Promise { const bank = group.getFirstBankByMint(mintPk); @@ -1852,7 +1849,7 @@ export class MangoClient { const flashLoanBeginIx = await this.program.methods .flashLoanBegin([ - toNativeDecimals(amountIn, inputBank.mintDecimals), + toNative(amountIn, inputBank.mintDecimals), new BN( 0, ) /* we don't care about borrowing the target amount, this is just a dummy */, diff --git a/ts/client/src/development.ts b/ts/client/src/development.ts new file mode 100644 index 000000000..a433f0857 --- /dev/null +++ b/ts/client/src/development.ts @@ -0,0 +1,76 @@ +/// +/// debugging +/// + +import { AccountMeta, PublicKey } from '@solana/web3.js'; +import { Bank } from './accounts/bank'; +import { Group } from './accounts/group'; +import { MangoAccount, Serum3Orders } from './accounts/mangoAccount'; +import { PerpMarket } from './accounts/perp'; + +export function debugAccountMetas(ams: AccountMeta[]): void { + for (const am of ams) { + console.log( + `${am.pubkey.toBase58()}, isSigner: ${am.isSigner + .toString() + .padStart(5, ' ')}, isWritable - ${am.isWritable + .toString() + .padStart(5, ' ')}`, + ); + } +} + +export function debugHealthAccounts( + group: Group, + mangoAccount: MangoAccount, + publicKeys: PublicKey[], +): void { + const banks = new Map( + Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [ + banks[0].publicKey.toBase58(), + `${banks[0].name} bank`, + ]), + ); + const oracles = new Map( + Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [ + banks[0].oracle.toBase58(), + `${banks[0].name} oracle`, + ]), + ); + const serum3 = new Map( + mangoAccount.serum3Active().map((serum3: Serum3Orders) => { + const serum3Market = Array.from( + group.serum3MarketsMapByExternal.values(), + ).find((serum3Market) => serum3Market.marketIndex === serum3.marketIndex); + if (!serum3Market) { + throw new Error( + `Serum3Orders for non existent market with market index ${serum3.marketIndex}`, + ); + } + return [serum3.openOrders.toBase58(), `${serum3Market.name} spot oo`]; + }), + ); + const perps = new Map( + Array.from(group.perpMarketsMapByName.values()).map( + (perpMarket: PerpMarket) => [ + perpMarket.publicKey.toBase58(), + `${perpMarket.name} perp market`, + ], + ), + ); + + publicKeys.map((pk) => { + if (banks.get(pk.toBase58())) { + console.log(banks.get(pk.toBase58())); + } + if (oracles.get(pk.toBase58())) { + console.log(oracles.get(pk.toBase58())); + } + if (serum3.get(pk.toBase58())) { + console.log(serum3.get(pk.toBase58())); + } + if (perps.get(pk.toBase58())) { + console.log(perps.get(pk.toBase58())); + } + }); +} diff --git a/ts/client/src/index.ts b/ts/client/src/index.ts index f906bec88..c732f4396 100644 --- a/ts/client/src/index.ts +++ b/ts/client/src/index.ts @@ -4,7 +4,7 @@ import { MangoClient } from './client'; import { MANGO_V4_ID } from './constants'; export * from './accounts/bank'; -export * from './accounts/I80F48'; +export * from './numbers/I80F48'; export * from './accounts/mangoAccount'; export { Serum3Market, diff --git a/ts/client/src/accounts/I80F48.ts b/ts/client/src/numbers/I80F48.ts similarity index 99% rename from ts/client/src/accounts/I80F48.ts rename to ts/client/src/numbers/I80F48.ts index 978a98848..ee4ce316a 100644 --- a/ts/client/src/accounts/I80F48.ts +++ b/ts/client/src/numbers/I80F48.ts @@ -45,7 +45,7 @@ export class I80F48 { } static fromNumber(x: number): I80F48 { const int_part = Math.trunc(x); - const v = new BN(int_part).iushln(48); + const v = new BN(int_part.toFixed(0)).iushln(48); v.iadd(new BN((x - int_part) * I80F48.MULTIPLIER_NUMBER)); return new I80F48(v); } diff --git a/ts/client/src/numbers/numbers.spec.ts b/ts/client/src/numbers/numbers.spec.ts new file mode 100644 index 000000000..ecb2d25fa --- /dev/null +++ b/ts/client/src/numbers/numbers.spec.ts @@ -0,0 +1,37 @@ +import BN from 'bn.js'; +import { expect } from 'chai'; +import { U64_MAX_BN } from '../utils'; +import { I80F48 } from './I80F48'; + +describe('Math', () => { + it('js number to BN and I80F48', () => { + // BN can be only be created from js numbers which are <=2^53 + expect(function () { + new BN(0x1fffffffffffff); + }).to.not.throw('Assertion failed'); + expect(function () { + new BN(0x20000000000000); + }).to.throw('Assertion failed'); + + // max BN cant be converted to a number + expect(function () { + U64_MAX_BN.toNumber(); + }).to.throw('Number can only safely store up to 53 bits'); + + // max I80F48 can be converted to a number + // though, the number is represented in scientific notation + // anything above ^20 gets represented with scientific notation + expect( + I80F48.fromString('604462909807314587353087.999999999999996') + .toNumber() + .toString(), + ).equals('6.044629098073146e+23'); + + // I80F48 constructor takes a BN, but it doesnt do what one might think it does + expect(new I80F48(new BN(10)).toNumber()).not.equals(10); + expect(I80F48.fromI64(new BN(10)).toNumber()).equals(10); + + // BN treats input as whole integer + expect(new BN(1.5).toNumber()).equals(1); + }); +}); diff --git a/ts/client/src/utils.ts b/ts/client/src/utils.ts index aa7e18cdf..16cf3a205 100644 --- a/ts/client/src/utils.ts +++ b/ts/client/src/utils.ts @@ -4,7 +4,6 @@ import { TOKEN_PROGRAM_ID, } from '@solana/spl-token'; import { - AccountMeta, AddressLookupTableAccount, MessageV0, PublicKey, @@ -14,107 +13,51 @@ import { VersionedTransaction, } from '@solana/web3.js'; import BN from 'bn.js'; -import { Bank, QUOTE_DECIMALS } from './accounts/bank'; -import { Group } from './accounts/group'; -import { I80F48 } from './accounts/I80F48'; -import { MangoAccount, Serum3Orders } from './accounts/mangoAccount'; -import { PerpMarket } from './accounts/perp'; +import { QUOTE_DECIMALS } from './accounts/bank'; +import { I80F48 } from './numbers/I80F48'; +/// +/// numeric helpers +/// export const U64_MAX_BN = new BN('18446744073709551615'); export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64); -// https://stackoverflow.com/questions/70261755/user-defined-type-guard-function-and-type-narrowing-to-more-specific-type/70262876#70262876 -export declare abstract class As { - private static readonly $as$: unique symbol; - private [As.$as$]: Record; +export function toNativeI80F48(uiAmount: number, decimals: number): I80F48 { + return I80F48.fromNumber(uiAmount * Math.pow(10, decimals)); } -export function debugAccountMetas(ams: AccountMeta[]): void { - for (const am of ams) { - console.log( - `${am.pubkey.toBase58()}, isSigner: ${am.isSigner - .toString() - .padStart(5, ' ')}, isWritable - ${am.isWritable - .toString() - .padStart(5, ' ')}`, - ); +export function toNative(uiAmount: number, decimals: number): BN { + return new BN((uiAmount * Math.pow(10, decimals)).toFixed(0)); +} + +export function toUiDecimals( + nativeAmount: BN | I80F48 | number, + decimals: number, +): number { + if (nativeAmount instanceof BN) { + return nativeAmount.div(new BN(Math.pow(10, decimals))).toNumber(); + } else if (nativeAmount instanceof I80F48) { + return nativeAmount + .div(I80F48.fromNumber(Math.pow(10, decimals))) + .toNumber(); } + return nativeAmount / Math.pow(10, decimals); } -export function debugHealthAccounts( - group: Group, - mangoAccount: MangoAccount, - publicKeys: PublicKey[], -): void { - const banks = new Map( - Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [ - banks[0].publicKey.toBase58(), - `${banks[0].name} bank`, - ]), - ); - const oracles = new Map( - Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [ - banks[0].oracle.toBase58(), - `${banks[0].name} oracle`, - ]), - ); - const serum3 = new Map( - mangoAccount.serum3Active().map((serum3: Serum3Orders) => { - const serum3Market = Array.from( - group.serum3MarketsMapByExternal.values(), - ).find((serum3Market) => serum3Market.marketIndex === serum3.marketIndex); - if (!serum3Market) { - throw new Error( - `Serum3Orders for non existent market with market index ${serum3.marketIndex}`, - ); - } - return [serum3.openOrders.toBase58(), `${serum3Market.name} spot oo`]; - }), - ); - const perps = new Map( - Array.from(group.perpMarketsMapByName.values()).map( - (perpMarket: PerpMarket) => [ - perpMarket.publicKey.toBase58(), - `${perpMarket.name} perp market`, - ], - ), - ); - - publicKeys.map((pk) => { - if (banks.get(pk.toBase58())) { - console.log(banks.get(pk.toBase58())); - } - if (oracles.get(pk.toBase58())) { - console.log(oracles.get(pk.toBase58())); - } - if (serum3.get(pk.toBase58())) { - console.log(serum3.get(pk.toBase58())); - } - if (perps.get(pk.toBase58())) { - console.log(perps.get(pk.toBase58())); - } - }); +export function toUiDecimalsForQuote( + nativeAmount: BN | I80F48 | number, +): number { + return toUiDecimals(nativeAmount, QUOTE_DECIMALS); } -export async function findOrCreate( - entityName: string, - findMethod: (...x: any) => any, - findArgs: any[], - createMethod: (...x: any) => any, - createArgs: any[], -): Promise { - let many: T[] = await findMethod(...findArgs); - let one: T; - if (many.length > 0) { - one = many[0]; - return one; - } - await createMethod(...createArgs); - many = await findMethod(...findArgs); - one = many[0]; - return one; +export function toUiI80F48(nativeAmount: I80F48, decimals: number): I80F48 { + return nativeAmount.div(I80F48.fromNumber(Math.pow(10, decimals))); } +/// +/// web3js extensions +/// + /** * Get the address of the associated token account for a given mint and owner * @@ -168,40 +111,6 @@ export async function createAssociatedTokenAccountIdempotentInstruction( }); } -export function toNative(uiAmount: number, decimals: number): I80F48 { - return I80F48.fromNumber(uiAmount).mul( - I80F48.fromNumber(Math.pow(10, decimals)), - ); -} - -export function toNativeDecimals(amount: number, decimals: number): BN { - return new BN(Math.trunc(amount * Math.pow(10, decimals))); -} - -export function toUiDecimals( - amount: I80F48 | number, - decimals: number, -): number { - amount = amount instanceof I80F48 ? amount.toNumber() : amount; - return amount / Math.pow(10, decimals); -} - -export function toUiDecimalsForQuote(amount: I80F48 | number): number { - amount = amount instanceof I80F48 ? amount.toNumber() : amount; - return amount / Math.pow(10, QUOTE_DECIMALS); -} - -export function toU64(amount: number, decimals: number): BN { - const bn = toNativeDecimals(amount, decimals).toString(); - console.log('bn', bn); - - return new BN(bn); -} - -export function nativeI80F48ToUi(amount: I80F48, decimals: number): I80F48 { - return amount.div(I80F48.fromNumber(Math.pow(10, decimals))); -} - export async function buildVersionedTx( provider: AnchorProvider, ix: TransactionInstruction[], @@ -222,3 +131,13 @@ export async function buildVersionedTx( ]); return vTx; } + +/// +/// ts extension +/// + +// https://stackoverflow.com/questions/70261755/user-defined-type-guard-function-and-type-narrowing-to-more-specific-type/70262876#70262876 +export declare abstract class As { + private static readonly $as$: unique symbol; + private [As.$as$]: Record; +} From b906e3dc7819dfed59ba6f96c50437003d6aa987 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Fri, 30 Sep 2022 13:21:01 +0200 Subject: [PATCH 32/39] Token deposit: Split into signed and permissionless ix Token accounts are a limited resource, so allowing other users to make use of them can cause problems. --- client/src/client.rs | 1 + .../src/instructions/token_deposit.rs | 224 ++++++++++++------ programs/mango-v4/src/lib.rs | 7 + .../tests/program_test/mango_client.rs | 71 ++++++ .../tests/program_test/mango_setup.rs | 1 + .../mango-v4/tests/test_bankrupt_tokens.rs | 7 + programs/mango-v4/tests/test_basic.rs | 1 + .../mango-v4/tests/test_health_compute.rs | 2 + programs/mango-v4/tests/test_health_region.rs | 2 + programs/mango-v4/tests/test_liq_perps.rs | 2 + programs/mango-v4/tests/test_liq_tokens.rs | 3 + programs/mango-v4/tests/test_margin_trade.rs | 1 + .../mango-v4/tests/test_perp_settle_fees.rs | 4 + .../mango-v4/tests/test_position_lifetime.rs | 34 ++- ts/client/src/client.ts | 1 + ts/client/src/mango_v4.ts | 112 +++++++++ 16 files changed, 402 insertions(+), 71 deletions(-) diff --git a/client/src/client.rs b/client/src/client.rs index cba8d94db..849831e47 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -344,6 +344,7 @@ impl MangoClient { &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, diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index e040dd9dc..17dd3bd29 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -11,8 +11,9 @@ use crate::util::checked_math as cm; use crate::logs::{DepositLog, TokenBalanceLog}; +// Same as TokenDeposit, but without the owner signing #[derive(Accounts)] -pub struct TokenDeposit<'info> { +pub struct TokenDepositIntoExisting<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group)] @@ -41,8 +42,50 @@ pub struct TokenDeposit<'info> { pub token_program: Program<'info, Token>, } -impl<'info> TokenDeposit<'info> { - pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> { +#[derive(Accounts)] +pub struct TokenDeposit<'info> { + pub group: AccountLoader<'info, Group>, + + #[account(mut, has_one = group, has_one = owner)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub owner: Signer<'info>, + + #[account( + mut, + has_one = group, + has_one = vault, + has_one = oracle, + // the mints of bank/vault/token_account are implicitly the same because + // spl::token::transfer succeeds between token_account and vault + )] + pub bank: AccountLoader<'info, Bank>, + + #[account(mut)] + pub vault: Account<'info, TokenAccount>, + + /// CHECK: The oracle can be one of several different account types + pub oracle: UncheckedAccount<'info>, + + #[account(mut)] + pub token_account: Box>, + pub token_authority: Signer<'info>, + + pub token_program: Program<'info, Token>, +} + +struct DepositCommon<'a, 'info> { + pub group: &'a AccountLoader<'info, Group>, + pub account: &'a AccountLoaderDynamic<'info, MangoAccount>, + pub bank: &'a AccountLoader<'info, Bank>, + pub vault: &'a Account<'info, TokenAccount>, + pub oracle: &'a UncheckedAccount<'info>, + pub token_account: &'a Box>, + pub token_authority: &'a Signer<'info>, + pub token_program: &'a Program<'info, Token>, +} + +impl<'a, 'info> DepositCommon<'a, 'info> { + fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> { let program = self.token_program.to_account_info(); let accounts = token::Transfer { from: self.token_account.to_account_info(), @@ -51,78 +94,119 @@ impl<'info> TokenDeposit<'info> { }; CpiContext::new(program, accounts) } + + fn deposit_into_existing( + &self, + remaining_accounts: &[AccountInfo], + amount: u64, + allow_token_account_closure: bool, + ) -> Result<()> { + require_msg!(amount > 0, "deposit amount must be positive"); + + let token_index = self.bank.load()?.token_index; + + // Get the account's position for that token index + let mut account = self.account.load_mut()?; + + let (position, raw_token_index) = account.token_position_mut(token_index)?; + + let amount_i80f48 = I80F48::from(amount); + let position_is_active = { + let mut bank = self.bank.load_mut()?; + bank.deposit(position, amount_i80f48)? + }; + + // Transfer the actual tokens + token::transfer(self.transfer_ctx(), amount)?; + + let indexed_position = position.indexed_position; + let bank = self.bank.load()?; + let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(self.oracle.as_ref())?)?; + + // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) + let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::(); + cm!(account.fixed.net_deposits += amount_usd); + + emit!(TokenBalanceLog { + mango_group: self.group.key(), + mango_account: self.account.key(), + token_index, + indexed_position: indexed_position.to_bits(), + deposit_index: bank.deposit_index.to_bits(), + borrow_index: bank.borrow_index.to_bits(), + }); + + // + // Health computation + // + // Since depositing can only increase health, we can skip the usual pre-health computation. + // Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated. + // + if !account.fixed.is_in_health_region() { + let retriever = + new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?; + let health = compute_health(&account.borrow(), HealthType::Init, &retriever) + .context("post-deposit init health")?; + msg!("health: {}", health); + account.fixed.maybe_recover_from_being_liquidated(health); + } + + // + // Deactivate the position only after the health check because the user passed in + // remaining_accounts for all banks/oracles, including the account that will now be + // deactivated. + // Deposits can deactivate a position if they cancel out a previous borrow. + // + if allow_token_account_closure && !position_is_active { + account.deactivate_token_position_and_log(raw_token_index, self.account.key()); + } + + emit!(DepositLog { + mango_group: self.group.key(), + mango_account: self.account.key(), + signer: self.token_authority.key(), + token_index, + quantity: amount, + price: oracle_price.to_bits(), + }); + + Ok(()) + } } pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { - require_msg!(amount > 0, "deposit amount must be positive"); - - let token_index = ctx.accounts.bank.load()?.token_index; - - // Get the account's position for that token index - let mut account = ctx.accounts.account.load_mut()?; - - let (position, raw_token_index, _active_token_index) = + { + let token_index = ctx.accounts.bank.load()?.token_index; + let mut account = ctx.accounts.account.load_mut()?; account.ensure_token_position(token_index)?; - - let amount_i80f48 = I80F48::from(amount); - let position_is_active = { - let mut bank = ctx.accounts.bank.load_mut()?; - bank.deposit(position, amount_i80f48)? - }; - - // Transfer the actual tokens - token::transfer(ctx.accounts.transfer_ctx(), amount)?; - - let indexed_position = position.indexed_position; - let bank = ctx.accounts.bank.load()?; - let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?; - - // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) - let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::(); - cm!(account.fixed.net_deposits += amount_usd); - - emit!(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index, - indexed_position: indexed_position.to_bits(), - deposit_index: bank.deposit_index.to_bits(), - borrow_index: bank.borrow_index.to_bits(), - }); - - // - // Health computation - // - // Since depositing can only increase health, we can skip the usual pre-health computation. - // Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated. - // - if !account.fixed.is_in_health_region() { - let retriever = - new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health = compute_health(&account.borrow(), HealthType::Init, &retriever) - .context("post-deposit init health")?; - msg!("health: {}", health); - account.fixed.maybe_recover_from_being_liquidated(health); } - // - // Deactivate the position only after the health check because the user passed in - // remaining_accounts for all banks/oracles, including the account that will now be - // deactivated. - // Deposits can deactivate a position if they cancel out a previous borrow. - // - if !position_is_active { - account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key()); + DepositCommon { + group: &ctx.accounts.group, + account: &ctx.accounts.account, + bank: &ctx.accounts.bank, + vault: &ctx.accounts.vault, + oracle: &ctx.accounts.oracle, + token_account: &ctx.accounts.token_account, + token_authority: &ctx.accounts.token_authority, + token_program: &ctx.accounts.token_program, } - - emit!(DepositLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - signer: ctx.accounts.token_authority.key(), - token_index, - quantity: amount, - price: oracle_price.to_bits(), - }); - - Ok(()) + .deposit_into_existing(ctx.remaining_accounts, amount, true) +} + +pub fn token_deposit_into_existing( + ctx: Context, + amount: u64, +) -> Result<()> { + DepositCommon { + group: &ctx.accounts.group, + account: &ctx.accounts.account, + bank: &ctx.accounts.bank, + vault: &ctx.accounts.vault, + oracle: &ctx.accounts.oracle, + token_account: &ctx.accounts.token_account, + token_authority: &ctx.accounts.token_authority, + token_program: &ctx.accounts.token_program, + } + .deposit_into_existing(ctx.remaining_accounts, amount, false) } diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 5dfe4fd97..72494cbb7 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -211,6 +211,13 @@ pub mod mango_v4 { instructions::token_deposit(ctx, amount) } + pub fn token_deposit_into_existing( + ctx: Context, + amount: u64, + ) -> Result<()> { + instructions::token_deposit_into_existing(ctx, amount) + } + pub fn token_withdraw( ctx: Context, amount: u64, diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index ccb5fcb56..94167d754 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -564,6 +564,7 @@ pub struct TokenDepositInstruction { pub amount: u64, pub account: Pubkey, + pub owner: TestKeypair, pub token_account: Pubkey, pub token_authority: TestKeypair, pub bank_index: usize, @@ -607,6 +608,76 @@ impl ClientInstruction for TokenDepositInstruction { ) .await; + let accounts = Self::Accounts { + group: account.fixed.group, + account: self.account, + owner: self.owner.pubkey(), + bank: mint_info.banks[self.bank_index], + vault: mint_info.vaults[self.bank_index], + oracle: mint_info.oracle, + token_account: self.token_account, + token_authority: self.token_authority.pubkey(), + token_program: Token::id(), + }; + + let mut instruction = make_instruction(program_id, &accounts, instruction); + instruction.accounts.extend(health_check_metas.into_iter()); + + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.token_authority, self.owner] + } +} + +pub struct TokenDepositIntoExistingInstruction { + pub amount: u64, + + pub account: Pubkey, + pub token_account: Pubkey, + pub token_authority: TestKeypair, + pub bank_index: usize, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for TokenDepositIntoExistingInstruction { + type Accounts = mango_v4::accounts::TokenDepositIntoExisting; + type Instruction = mango_v4::instruction::TokenDepositIntoExisting; + async fn to_instruction( + &self, + account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction { + amount: self.amount, + }; + + // load account so we know its mint + let token_account: TokenAccount = account_loader.load(&self.token_account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); + let mint_info = Pubkey::find_program_address( + &[ + b"MintInfo".as_ref(), + account.fixed.group.as_ref(), + token_account.mint.as_ref(), + ], + &program_id, + ) + .0; + let mint_info: MintInfo = account_loader.load(&mint_info).await.unwrap(); + + let health_check_metas = derive_health_check_remaining_account_metas( + &account_loader, + &account, + Some(mint_info.banks[self.bank_index]), + false, + None, + ) + .await; + let accounts = Self::Accounts { group: account.fixed.group, account: self.account, diff --git a/programs/mango-v4/tests/program_test/mango_setup.rs b/programs/mango-v4/tests/program_test/mango_setup.rs index a5b949f2d..3cc8f49a9 100644 --- a/programs/mango-v4/tests/program_test/mango_setup.rs +++ b/programs/mango-v4/tests/program_test/mango_setup.rs @@ -178,6 +178,7 @@ pub async fn create_funded_account( TokenDepositInstruction { amount: amounts, account, + owner, token_account: payer.token_accounts[mint.index], token_authority: payer.key, bank_index, diff --git a/programs/mango-v4/tests/test_bankrupt_tokens.rs b/programs/mango-v4/tests/test_bankrupt_tokens.rs index 755d7621a..6fb130f23 100644 --- a/programs/mango-v4/tests/test_bankrupt_tokens.rs +++ b/programs/mango-v4/tests/test_bankrupt_tokens.rs @@ -59,6 +59,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { TokenDepositInstruction { amount: 10, account: vault_account, + owner, token_account: payer_mint_accounts[0], token_authority: payer.clone(), bank_index: 0, @@ -94,6 +95,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit1_amount, account, + owner, token_account: payer_mint_accounts[2], token_authority: payer.clone(), bank_index: 0, @@ -106,6 +108,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit2_amount, account, + owner, token_account: payer_mint_accounts[3], token_authority: payer.clone(), bank_index: 0, @@ -354,6 +357,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { TokenDepositInstruction { amount: vault_amount, account: vault_account, + owner, token_account, token_authority: payer.clone(), bank_index: 1, @@ -369,6 +373,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { TokenDepositInstruction { amount: 10, account: vault_account, + owner, token_account: payer_mint_accounts[0], token_authority: payer.clone(), bank_index: 0, @@ -404,6 +409,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit1_amount, account, + owner, token_account: payer_mint_accounts[2], token_authority: payer.clone(), bank_index: 0, @@ -416,6 +422,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit2_amount, account, + owner, token_account: payer_mint_accounts[3], token_authority: payer.clone(), bank_index: 0, diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index 1a733e287..9a78bae97 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -84,6 +84,7 @@ async fn test_basic() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit_amount, account, + owner, token_account: payer_mint0_account, token_authority: payer.clone(), bank_index: 0, diff --git a/programs/mango-v4/tests/test_health_compute.rs b/programs/mango-v4/tests/test_health_compute.rs index 656e2e534..03fd91773 100644 --- a/programs/mango-v4/tests/test_health_compute.rs +++ b/programs/mango-v4/tests/test_health_compute.rs @@ -145,6 +145,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> { TokenDepositInstruction { amount: 10, account, + owner, token_account: payer_mint_accounts[0], token_authority: payer.clone(), bank_index: 0, @@ -264,6 +265,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> { TokenDepositInstruction { amount: 10, account, + owner, token_account: payer_mint_accounts[0], token_authority: payer.clone(), bank_index: 0, diff --git a/programs/mango-v4/tests/test_health_region.rs b/programs/mango-v4/tests/test_health_region.rs index 80e7eb8ce..6f58d7822 100644 --- a/programs/mango-v4/tests/test_health_region.rs +++ b/programs/mango-v4/tests/test_health_region.rs @@ -63,6 +63,7 @@ async fn test_health_wrap() -> Result<(), TransportError> { TokenDepositInstruction { amount: 1, account, + owner, token_account: payer_mint_accounts[0], token_authority: payer.clone(), bank_index: 0, @@ -89,6 +90,7 @@ async fn test_health_wrap() -> Result<(), TransportError> { tx.add_instruction(TokenDepositInstruction { amount: repay_amount, account, + owner, token_account: payer_mint_accounts[1], token_authority: payer.clone(), bank_index: 0, diff --git a/programs/mango-v4/tests/test_liq_perps.rs b/programs/mango-v4/tests/test_liq_perps.rs index 693d0039a..2275c1f57 100644 --- a/programs/mango-v4/tests/test_liq_perps.rs +++ b/programs/mango-v4/tests/test_liq_perps.rs @@ -94,6 +94,7 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> { TokenDepositInstruction { amount: 1, account, + owner, token_account: payer_mint_accounts[1], token_authority: payer, bank_index: 0, @@ -307,6 +308,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr TokenDepositInstruction { amount: 1, account, + owner, token_account: payer_mint_accounts[1], token_authority: payer, bank_index: 0, diff --git a/programs/mango-v4/tests/test_liq_tokens.rs b/programs/mango-v4/tests/test_liq_tokens.rs index bb9315141..88f13db63 100644 --- a/programs/mango-v4/tests/test_liq_tokens.rs +++ b/programs/mango-v4/tests/test_liq_tokens.rs @@ -233,6 +233,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { TokenDepositInstruction { amount: 100000, account: vault_account, + owner, token_account, token_authority: payer.clone(), bank_index: 0, @@ -269,6 +270,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit1_amount, account, + owner, token_account: payer_mint_accounts[2], token_authority: payer.clone(), bank_index: 0, @@ -281,6 +283,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit2_amount, account, + owner, token_account: payer_mint_accounts[3], token_authority: payer.clone(), bank_index: 0, diff --git a/programs/mango-v4/tests/test_margin_trade.rs b/programs/mango-v4/tests/test_margin_trade.rs index bcb8242ee..65bc68111 100644 --- a/programs/mango-v4/tests/test_margin_trade.rs +++ b/programs/mango-v4/tests/test_margin_trade.rs @@ -89,6 +89,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> { TokenDepositInstruction { amount: deposit_amount_initial, account, + owner, token_account: payer_mint0_account, token_authority: payer.clone(), bank_index: 0, diff --git a/programs/mango-v4/tests/test_perp_settle_fees.rs b/programs/mango-v4/tests/test_perp_settle_fees.rs index b4b7e41f5..7359078bf 100644 --- a/programs/mango-v4/tests/test_perp_settle_fees.rs +++ b/programs/mango-v4/tests/test_perp_settle_fees.rs @@ -80,6 +80,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit_amount, account: account_0, + owner, token_account: payer_mint_accounts[0], token_authority: payer.clone(), bank_index: 0, @@ -93,6 +94,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit_amount, account: account_0, + owner, token_account: payer_mint_accounts[1], token_authority: payer.clone(), bank_index: 0, @@ -110,6 +112,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit_amount, account: account_1, + owner, token_account: payer_mint_accounts[0], token_authority: payer.clone(), bank_index: 0, @@ -123,6 +126,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { TokenDepositInstruction { amount: deposit_amount, account: account_1, + owner, token_account: payer_mint_accounts[1], token_authority: payer.clone(), bank_index: 0, diff --git a/programs/mango-v4/tests/test_position_lifetime.rs b/programs/mango-v4/tests/test_position_lifetime.rs index ad982691f..8913dd725 100644 --- a/programs/mango-v4/tests/test_position_lifetime.rs +++ b/programs/mango-v4/tests/test_position_lifetime.rs @@ -71,14 +71,30 @@ async fn test_position_lifetime() -> Result<()> { { let start_balance = solana.token_account_balance(payer_mint_accounts[0]).await; - // this activates the positions let deposit_amount = 100; + + // cannot deposit_into_existing if no token deposit exists + assert!(send_tx( + solana, + TokenDepositIntoExistingInstruction { + amount: deposit_amount, + account, + token_account: payer_mint_accounts[0], + token_authority: payer, + bank_index: 0, + } + ) + .await + .is_err()); + + // this activates the positions for &payer_token in payer_mint_accounts { send_tx( solana, TokenDepositInstruction { amount: deposit_amount, account, + owner, token_account: payer_token, token_authority: payer.clone(), bank_index: 0, @@ -88,6 +104,20 @@ async fn test_position_lifetime() -> Result<()> { .unwrap(); } + // now depositing into an active account works + send_tx( + solana, + TokenDepositIntoExistingInstruction { + amount: deposit_amount, + account, + token_account: payer_mint_accounts[0], + token_authority: payer, + bank_index: 0, + }, + ) + .await + .unwrap(); + // this closes the positions for &payer_token in payer_mint_accounts { send_tx( @@ -131,6 +161,7 @@ async fn test_position_lifetime() -> Result<()> { TokenDepositInstruction { amount: collateral_amount, account, + owner, token_account: payer_mint_accounts[0], token_authority: payer.clone(), bank_index: 0, @@ -167,6 +198,7 @@ async fn test_position_lifetime() -> Result<()> { // deposit withdraw amount + some more to cover loan origination fees amount: borrow_amount + 2, account, + owner, token_account: payer_mint_accounts[1], token_authority: payer.clone(), bank_index: 0, diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 2d5e762a2..364c7fb9a 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -809,6 +809,7 @@ export class MangoClient { .accounts({ group: group.publicKey, account: mangoAccount.publicKey, + owner: mangoAccount.owner, bank: bank.publicKey, vault: bank.vault, oracle: bank.oracle, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 03b01928f..9292bb01c 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -1076,6 +1076,62 @@ export type MangoV4 = { }, { "name": "tokenDeposit", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "bank", + "isMut": true, + "isSigner": false + }, + { + "name": "vault", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "tokenDepositIntoExisting", "accounts": [ { "name": "group", @@ -7282,6 +7338,62 @@ export const IDL: MangoV4 = { }, { "name": "tokenDeposit", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "bank", + "isMut": true, + "isSigner": false + }, + { + "name": "vault", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "tokenDepositIntoExisting", "accounts": [ { "name": "group", From 5b549fb59f11a376a67fc84b2741502a51ea91b9 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Mon, 3 Oct 2022 13:06:04 +0200 Subject: [PATCH 33/39] program: fix bug where expansion would not move positions whose size remained same (#262) Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- programs/mango-v4/src/state/mango_account.rs | 37 +++++++++----------- programs/mango-v4/tests/test_basic.rs | 24 ++++++++++++- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index f16bacf2a..2a57970ea 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -974,7 +974,7 @@ impl< // expand dynamic components by first moving existing positions, and then setting new ones to defaults // perp oo - if new_header.perp_oo_count() > old_header.perp_oo_count() { + if old_header.perp_oo_count() > 0 { unsafe { sol_memmove( &mut dynamic[new_header.perp_oo_offset(0)], @@ -982,14 +982,14 @@ impl< size_of::() * old_header.perp_oo_count(), ); } - for i in old_header.perp_oo_count..new_perp_oo_count { - *get_helper_mut(dynamic, new_header.perp_oo_offset(i.into())) = - PerpOpenOrder::default(); - } + } + for i in old_header.perp_oo_count..new_perp_oo_count { + *get_helper_mut(dynamic, new_header.perp_oo_offset(i.into())) = + PerpOpenOrder::default(); } // perp positions - if new_header.perp_count() > old_header.perp_count() { + if old_header.perp_count() > 0 { unsafe { sol_memmove( &mut dynamic[new_header.perp_offset(0)], @@ -997,14 +997,13 @@ impl< size_of::() * old_header.perp_count(), ); } - for i in old_header.perp_count..new_perp_count { - *get_helper_mut(dynamic, new_header.perp_offset(i.into())) = - PerpPosition::default(); - } + } + for i in old_header.perp_count..new_perp_count { + *get_helper_mut(dynamic, new_header.perp_offset(i.into())) = PerpPosition::default(); } // serum3 positions - if new_header.serum3_count() > old_header.serum3_count() { + if old_header.serum3_count() > 0 { unsafe { sol_memmove( &mut dynamic[new_header.serum3_offset(0)], @@ -1012,14 +1011,13 @@ impl< size_of::() * old_header.serum3_count(), ); } - for i in old_header.serum3_count..new_serum3_count { - *get_helper_mut(dynamic, new_header.serum3_offset(i.into())) = - Serum3Orders::default(); - } + } + for i in old_header.serum3_count..new_serum3_count { + *get_helper_mut(dynamic, new_header.serum3_offset(i.into())) = Serum3Orders::default(); } // token positions - if new_header.token_count() > old_header.token_count() { + if old_header.token_count() > 0 { unsafe { sol_memmove( &mut dynamic[new_header.token_offset(0)], @@ -1027,10 +1025,9 @@ impl< size_of::() * old_header.token_count(), ); } - for i in old_header.token_count..new_token_count { - *get_helper_mut(dynamic, new_header.token_offset(i.into())) = - TokenPosition::default(); - } + } + for i in old_header.token_count..new_token_count { + *get_helper_mut(dynamic, new_header.token_offset(i.into())) = TokenPosition::default(); } // update the already-parsed header diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index 9a78bae97..0db9978f1 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -43,7 +43,7 @@ async fn test_basic() -> Result<(), TransportError> { AccountCreateInstruction { account_num: 0, token_count: 8, - serum3_count: 0, + serum3_count: 7, perp_count: 0, perp_oo_count: 0, group, @@ -54,6 +54,17 @@ async fn test_basic() -> Result<(), TransportError> { .await .unwrap() .account; + let account_data: MangoAccount = solana.get_account(account).await; + assert_eq!(account_data.tokens.len(), 8); + assert_eq!( + account_data.tokens.iter().filter(|t| t.is_active()).count(), + 0 + ); + assert_eq!(account_data.serum3.len(), 7); + assert_eq!( + account_data.serum3.iter().filter(|s| s.is_active()).count(), + 0 + ); send_tx( solana, @@ -71,6 +82,17 @@ async fn test_basic() -> Result<(), TransportError> { .await .unwrap() .account; + let account_data: MangoAccount = solana.get_account(account).await; + assert_eq!(account_data.tokens.len(), 16); + assert_eq!( + account_data.tokens.iter().filter(|t| t.is_active()).count(), + 0 + ); + assert_eq!(account_data.serum3.len(), 8); + assert_eq!( + account_data.serum3.iter().filter(|s| s.is_active()).count(), + 0 + ); // // TEST: Deposit funds From 8cdd0233d43ca248f288eb58c84417051a352c87 Mon Sep 17 00:00:00 2001 From: tjs Date: Tue, 4 Oct 2022 15:13:24 -0400 Subject: [PATCH 34/39] fix merge issues --- ts/client/src/accounts/healthCache.ts | 3 --- ts/client/src/accounts/mangoAccount.ts | 2 +- ts/client/src/accounts/serum3.ts | 6 ------ 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 536a14d43..e3dc93af8 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -278,9 +278,6 @@ export class HealthCache { const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank); const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank); - const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank); - const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank); - const baseEntry = this.tokenInfos[baseEntryIndex]; const reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice); diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 99807f4cf..94aaab350 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -1,7 +1,7 @@ import { BN } from '@project-serum/anchor'; import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market'; -import { PublicKey } from '@solana/web3.js'; +import { AccountInfo, PublicKey } from '@solana/web3.js'; import { MangoClient } from '../client'; import { SERUM3_PROGRAM_ID } from '../constants'; import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48'; diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index 31b251c1d..38cbc83eb 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -95,12 +95,6 @@ export class Serum3Market { .toNumber(); } - public getSerum3ExternalMarket(group: Group) { - return group.serum3MarketExternalsMap.get( - this.serumMarketExternal.toBase58(), - ); - } - public async loadBids(client: MangoClient, group: Group): Promise { const serum3MarketExternal = group.getSerum3ExternalMarket( this.serumMarketExternal, From dd4175b40124fc1d353028e4570a20c141fcb165 Mon Sep 17 00:00:00 2001 From: tjs Date: Tue, 4 Oct 2022 16:35:38 -0400 Subject: [PATCH 35/39] add back get serum3marketbypk --- ts/client/src/accounts/group.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 86e757b18..e3ebc34f4 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -455,6 +455,16 @@ export class Group { return serum3Market; } + public getSerum3MarketByPk(pk: PublicKey): Serum3Market | undefined { + const serum3Market = Array.from( + this.serum3MarketsMapByExternal.values(), + ).find((serum3Market) => serum3Market.serumMarketExternal.equals(pk)); + if (!serum3Market) { + throw new Error(`No serum3Market found by public key ${pk}!`); + } + return serum3Market; + } + public getSerum3MarketByExternalMarket( externalMarketPk: PublicKey, ): Serum3Market { From f2cc8697959ade82d2ea631e7254c24439f3ea74 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 29 Sep 2022 14:13:28 +0200 Subject: [PATCH 36/39] Perps: Prepare for settlement token != oracle token Make sure the setting is available and that the needed accounts are passed to the perp instructions. --- anchor-tests/test.ts | 2 +- client/src/client.rs | 14 ++-- .../src/instructions/perp_create_market.rs | 25 +++++-- .../instructions/perp_deactivate_position.rs | 5 +- .../src/instructions/perp_liq_bankruptcy.rs | 29 +++++--- .../instructions/perp_liq_base_position.rs | 2 +- .../src/instructions/perp_place_order.rs | 12 +++- .../src/instructions/perp_settle_fees.rs | 19 +++-- .../src/instructions/perp_settle_pnl.rs | 28 +++++--- programs/mango-v4/src/lib.rs | 2 + programs/mango-v4/src/state/orderbook/mod.rs | 12 ++-- programs/mango-v4/src/state/perp_market.rs | 7 +- .../tests/program_test/mango_client.rs | 19 +++-- programs/mango-v4/tests/test_liq_perps.rs | 2 +- programs/mango-v4/tests/test_perp.rs | 4 +- programs/mango-v4/tests/test_perp_settle.rs | 30 ++++---- .../mango-v4/tests/test_perp_settle_fees.rs | 14 ++-- ts/client/src/client.ts | 3 +- ts/client/src/mango_v4.ts | 72 +++++++++++++------ ts/client/src/scripts/devnet-admin.ts | 2 +- .../src/scripts/mb-liqtest-create-group.ts | 2 +- 21 files changed, 190 insertions(+), 115 deletions(-) diff --git a/anchor-tests/test.ts b/anchor-tests/test.ts index eaf6c63ec..f266242b0 100644 --- a/anchor-tests/test.ts +++ b/anchor-tests/test.ts @@ -185,7 +185,6 @@ describe('mango-v4', () => { 'BTC-PERP', 0.1, 6, - 1, 10, 100, 0.975, @@ -204,6 +203,7 @@ describe('mango-v4', () => { 0, 0, 0, + 0, ); await group.reloadAll(envClient); }); diff --git a/client/src/client.rs b/client/src/client.rs index 849831e47..51537745f 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -811,7 +811,7 @@ impl MangoClient { account_b: (&Pubkey, &MangoAccountValue), ) -> anyhow::Result { let perp = self.context.perp(market_index); - let settlement_token = self.context.token(0); + let settlement_token = self.context.token(perp.market.settle_token_index); let health_remaining_ams = self .context @@ -832,7 +832,8 @@ impl MangoClient { account_a: *account_a.0, account_b: *account_b.0, oracle: perp.market.oracle, - quote_bank: settlement_token.mint_info.first_bank(), + settle_bank: settlement_token.mint_info.first_bank(), + settle_oracle: settlement_token.mint_info.oracle, }, None, ); @@ -931,15 +932,13 @@ impl MangoClient { market_index: PerpMarketIndex, max_liab_transfer: u64, ) -> anyhow::Result { - let quote_token_index = 0; - let quote_info = self.context.token(quote_token_index); - let group = account_fetcher_fetch_anchor_account::( &*self.account_fetcher, &self.context.group, )?; 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, &[]) @@ -957,8 +956,9 @@ impl MangoClient { liqor: self.mango_account_address, liqor_owner: self.owner(), liqee: *liqee.0, - quote_bank: quote_info.mint_info.first_bank(), - quote_vault: quote_info.mint_info.first_vault(), + 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(), }, diff --git a/programs/mango-v4/src/instructions/perp_create_market.rs b/programs/mango-v4/src/instructions/perp_create_market.rs index 43fa71e74..1ad67caab 100644 --- a/programs/mango-v4/src/instructions/perp_create_market.rs +++ b/programs/mango-v4/src/instructions/perp_create_market.rs @@ -1,8 +1,7 @@ use anchor_lang::prelude::*; use fixed::types::I80F48; -use crate::error::MangoError; - +use crate::error::*; use crate::state::*; use crate::util::fill_from_str; @@ -46,6 +45,7 @@ pub struct PerpCreateMarket<'info> { #[allow(clippy::too_many_arguments)] pub fn perp_create_market( ctx: Context, + settle_token_index: TokenIndex, perp_market_index: PerpMarketIndex, name: String, oracle_config: OracleConfig, @@ -69,10 +69,25 @@ pub fn perp_create_market( settle_fee_amount_threshold: f32, settle_fee_fraction_low_health: f32, ) -> Result<()> { + // Settlement tokens that aren't USDC aren't fully implemented, the main missing steps are: + // - In health: the perp health needs to be adjusted by the settlement token weights. + // Otherwise settling perp pnl could decrease health. + // - In settle pnl and settle fees: use the settle oracle to convert the pnl from USD to token. + // - In perp bankruptcy: fix the assumption that the insurance fund has the same mint as + // the settlement token. + require_msg!( + settle_token_index == QUOTE_TOKEN_INDEX, + "settlement tokens != USDC are not fully implemented" + ); + let mut perp_market = ctx.accounts.perp_market.load_init()?; *perp_market = PerpMarket { - name: fill_from_str(&name)?, group: ctx.accounts.group.key(), + settle_token_index, + perp_market_index, + group_insurance_fund: if group_insurance_fund { 1 } else { 0 }, + trusted_market: if trusted_market { 1 } else { 0 }, + name: fill_from_str(&name)?, oracle: ctx.accounts.oracle.key(), oracle_config, bids: ctx.accounts.bids.key(), @@ -99,11 +114,7 @@ pub fn perp_create_market( fees_settled: I80F48::ZERO, bump: *ctx.bumps.get("perp_market").ok_or(MangoError::SomeError)?, base_decimals, - perp_market_index, registration_time: Clock::get()?.unix_timestamp, - group_insurance_fund: if group_insurance_fund { 1 } else { 0 }, - trusted_market: if trusted_market { 1 } else { 0 }, - padding0: Default::default(), padding1: Default::default(), padding2: Default::default(), fee_penalty, diff --git a/programs/mango-v4/src/instructions/perp_deactivate_position.rs b/programs/mango-v4/src/instructions/perp_deactivate_position.rs index 59e4de272..2841c3fe0 100644 --- a/programs/mango-v4/src/instructions/perp_deactivate_position.rs +++ b/programs/mango-v4/src/instructions/perp_deactivate_position.rs @@ -50,7 +50,10 @@ pub fn perp_deactivate_position(ctx: Context) -> Result< "perp position still has events on event queue" ); - account.deactivate_perp_position(perp_market.perp_market_index, QUOTE_TOKEN_INDEX)?; + account.deactivate_perp_position( + perp_market.perp_market_index, + perp_market.settle_token_index, + )?; Ok(()) } diff --git a/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs index dc6f874ef..33b208e32 100644 --- a/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs @@ -38,13 +38,19 @@ pub struct PerpLiqBankruptcy<'info> { #[account( mut, has_one = group, - constraint = quote_bank.load()?.vault == quote_vault.key() // address is checked at #2 )] - pub quote_bank: AccountLoader<'info, Bank>, + pub settle_bank: AccountLoader<'info, Bank>, - #[account(mut)] - pub quote_vault: Account<'info, TokenAccount>, + #[account( + mut, + address = settle_bank.load()?.vault + )] + pub settle_vault: Account<'info, TokenAccount>, + + /// CHECK: Oracle can have different account types + #[account(address = settle_bank.load()?.oracle)] + pub settle_oracle: UncheckedAccount<'info>, // future: this would be an insurance fund vault specific to a // trustless token, separate from the shared one on the group @@ -59,7 +65,7 @@ impl<'info> PerpLiqBankruptcy<'info> { let program = self.token_program.to_account_info(); let accounts = token::Transfer { from: self.insurance_vault.to_account_info(), - to: self.quote_vault.to_account_info(), + to: self.settle_vault.to_account_info(), authority: self.group.to_account_info(), }; CpiContext::new(program, accounts) @@ -96,6 +102,7 @@ pub fn perp_liq_bankruptcy(ctx: Context, max_liab_transfer: u // Find bankrupt liab amount let mut perp_market = ctx.accounts.perp_market.load_mut()?; + let settle_token_index = perp_market.settle_token_index; let liqee_perp_position = liqee.perp_position_mut(perp_market.perp_market_index)?; require_msg!( liqee_perp_position.base_position_lots() == 0, @@ -137,9 +144,9 @@ pub fn perp_liq_bankruptcy(ctx: Context, max_liab_transfer: u // Try using the insurance fund if possible if insurance_transfer > 0 { - let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - require_eq!(quote_bank.token_index, QUOTE_TOKEN_INDEX); - require_keys_eq!(quote_bank.mint, ctx.accounts.insurance_vault.mint); + let mut settle_bank = ctx.accounts.settle_bank.load_mut()?; + require_eq!(settle_bank.token_index, settle_token_index); + require_keys_eq!(settle_bank.mint, ctx.accounts.insurance_vault.mint); // move insurance assets into quote bank let group_seeds = group_seeds!(group); @@ -149,12 +156,12 @@ pub fn perp_liq_bankruptcy(ctx: Context, max_liab_transfer: u )?; // credit the liqor with quote tokens - let (liqor_quote, _, _) = liqor.ensure_token_position(QUOTE_TOKEN_INDEX)?; - quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?; + let (liqor_quote, _, _) = liqor.ensure_token_position(settle_token_index)?; + settle_bank.deposit(liqor_quote, insurance_transfer_i80f48)?; // transfer perp quote loss from the liqee to the liqor let liqor_perp_position = liqor - .ensure_perp_position(perp_market.perp_market_index, QUOTE_TOKEN_INDEX)? + .ensure_perp_position(perp_market.perp_market_index, settle_token_index)? .0; liqee_perp_position.change_quote_position(insurance_liab_transfer); liqor_perp_position.change_quote_position(-insurance_liab_transfer); diff --git a/programs/mango-v4/src/instructions/perp_liq_base_position.rs b/programs/mango-v4/src/instructions/perp_liq_base_position.rs index 70feab58b..b8e46cca0 100644 --- a/programs/mango-v4/src/instructions/perp_liq_base_position.rs +++ b/programs/mango-v4/src/instructions/perp_liq_base_position.rs @@ -87,7 +87,7 @@ pub fn perp_liq_base_position( // Fetch perp positions for accounts, creating for the liqor if needed let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?; let liqor_perp_position = liqor - .ensure_perp_position(perp_market_index, QUOTE_TOKEN_INDEX)? + .ensure_perp_position(perp_market_index, perp_market.settle_token_index)? .0; let liqee_base_lots = liqee_perp_position.base_position_lots(); diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index 8f8edfb11..e73331045 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -5,7 +5,7 @@ use crate::error::*; use crate::state::MangoAccount; use crate::state::{ new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, Book, BookSide, - EventQueue, Group, OrderType, PerpMarket, Side, QUOTE_TOKEN_INDEX, + EventQueue, Group, OrderType, PerpMarket, Side, }; #[derive(Accounts)] @@ -83,12 +83,18 @@ pub fn perp_place_order( let account_pk = ctx.accounts.account.key(); - let perp_market_index = ctx.accounts.perp_market.load()?.perp_market_index; + let (perp_market_index, settle_token_index) = { + let perp_market = ctx.accounts.perp_market.load()?; + ( + perp_market.perp_market_index, + perp_market.settle_token_index, + ) + }; // // Create the perp position if needed // - account.ensure_perp_position(perp_market_index, QUOTE_TOKEN_INDEX)?; + account.ensure_perp_position(perp_market_index, settle_token_index)?; // // Pre-health computation, _after_ perp position is created diff --git a/programs/mango-v4/src/instructions/perp_settle_fees.rs b/programs/mango-v4/src/instructions/perp_settle_fees.rs index c5fece123..f0a9ef7d0 100644 --- a/programs/mango-v4/src/instructions/perp_settle_fees.rs +++ b/programs/mango-v4/src/instructions/perp_settle_fees.rs @@ -9,7 +9,6 @@ use crate::state::new_fixed_order_account_retriever; use crate::state::Bank; use crate::state::HealthType; use crate::state::MangoAccount; -use crate::state::QUOTE_TOKEN_INDEX; use crate::state::{AccountLoaderDynamic, Group, PerpMarket}; #[derive(Accounts)] @@ -27,7 +26,11 @@ pub struct PerpSettleFees<'info> { pub oracle: UncheckedAccount<'info>, #[account(mut, has_one = group)] - pub quote_bank: AccountLoader<'info, Bank>, + pub settle_bank: AccountLoader<'info, Bank>, + + /// CHECK: Oracle can have different account types + #[account(address = settle_bank.load()?.oracle)] + pub settle_oracle: UncheckedAccount<'info>, } pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> Result<()> { @@ -38,12 +41,13 @@ pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> ); let mut account = ctx.accounts.account.load_mut()?; - let mut bank = ctx.accounts.quote_bank.load_mut()?; + let mut bank = ctx.accounts.settle_bank.load_mut()?; let mut perp_market = ctx.accounts.perp_market.load_mut()?; // Verify that the bank is the quote currency bank - require!( - bank.token_index == QUOTE_TOKEN_INDEX, + require_eq!( + bank.token_index, + perp_market.settle_token_index, MangoError::InvalidBank ); @@ -81,8 +85,9 @@ pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> account.fixed.net_settled = cm!(account.fixed.net_settled - settlement_i64); // Transfer token balances - // TODO: Need to guarantee that QUOTE_TOKEN_INDEX token exists at this point. I.E. create it when placing perp order. - let token_position = account.ensure_token_position(QUOTE_TOKEN_INDEX)?.0; + let token_position = account + .token_position_mut(perp_market.settle_token_index)? + .0; bank.withdraw_with_fee(token_position, settlement)?; // Update the settled balance on the market itself perp_market.fees_settled = cm!(perp_market.fees_settled + settlement); diff --git a/programs/mango-v4/src/instructions/perp_settle_pnl.rs b/programs/mango-v4/src/instructions/perp_settle_pnl.rs index f29df0103..e2a4e531a 100644 --- a/programs/mango-v4/src/instructions/perp_settle_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_settle_pnl.rs @@ -9,7 +9,6 @@ use crate::state::Bank; use crate::state::HealthType; use crate::state::MangoAccount; use crate::state::ScanningAccountRetriever; -use crate::state::QUOTE_TOKEN_INDEX; use crate::state::{AccountLoaderDynamic, Group, PerpMarket}; #[derive(Accounts)] @@ -38,7 +37,11 @@ pub struct PerpSettlePnl<'info> { pub oracle: UncheckedAccount<'info>, #[account(mut, has_one = group)] - pub quote_bank: AccountLoader<'info, Bank>, + pub settle_bank: AccountLoader<'info, Bank>, + + /// CHECK: Oracle can have different account types + #[account(address = settle_bank.load()?.oracle)] + pub settle_oracle: UncheckedAccount<'info>, } pub fn perp_settle_pnl(ctx: Context) -> Result<()> { @@ -48,9 +51,12 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { MangoError::CannotSettleWithSelf ); - let perp_market_index = { + let (perp_market_index, settle_token_index) = { let perp_market = ctx.accounts.perp_market.load()?; - perp_market.perp_market_index + ( + perp_market.perp_market_index, + perp_market.settle_token_index, + ) }; let mut account_a = ctx.accounts.account_a.load_mut()?; @@ -59,9 +65,9 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { // check positions exist, for nicer error messages { account_a.perp_position(perp_market_index)?; - account_a.token_position(QUOTE_TOKEN_INDEX)?; + account_a.token_position(settle_token_index)?; account_b.perp_position(perp_market_index)?; - account_b.token_position(QUOTE_TOKEN_INDEX)?; + account_b.token_position(settle_token_index)?; } let a_init_health; @@ -84,12 +90,12 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { // Further settlement would convert perp-losses into token-losses and isn't allowed. require!(b_settle_health >= 0, MangoError::HealthMustBePositive); - let mut bank = ctx.accounts.quote_bank.load_mut()?; + let mut bank = ctx.accounts.settle_bank.load_mut()?; let perp_market = ctx.accounts.perp_market.load()?; // Verify that the bank is the quote currency bank require!( - bank.token_index == QUOTE_TOKEN_INDEX, + bank.token_index == settle_token_index, MangoError::InvalidBank ); @@ -153,8 +159,8 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { // Transfer token balances // The fee is paid by the account with positive unsettled pnl - let a_token_position = account_a.token_position_mut(QUOTE_TOKEN_INDEX)?.0; - let b_token_position = account_b.token_position_mut(QUOTE_TOKEN_INDEX)?.0; + let a_token_position = account_a.token_position_mut(settle_token_index)?.0; + let b_token_position = account_b.token_position_mut(settle_token_index)?.0; bank.deposit(a_token_position, cm!(settlement - fee))?; bank.withdraw_with_fee(b_token_position, settlement)?; @@ -172,7 +178,7 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { ); let (settler_token_position, settler_token_raw_index, _) = - settler.ensure_token_position(QUOTE_TOKEN_INDEX)?; + settler.ensure_token_position(settle_token_index)?; if !bank.deposit(settler_token_position, fee)? { settler.deactivate_token_position(settler_token_raw_index); } diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 72494cbb7..a96c93d3d 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -404,9 +404,11 @@ pub mod mango_v4 { settle_fee_flat: f32, settle_fee_amount_threshold: f32, settle_fee_fraction_low_health: f32, + settle_token_index: TokenIndex, ) -> Result<()> { instructions::perp_create_market( ctx, + settle_token_index, perp_market_index, name, oracle_config, diff --git a/programs/mango-v4/src/state/orderbook/mod.rs b/programs/mango-v4/src/state/orderbook/mod.rs index 9f4161df4..b28378613 100644 --- a/programs/mango-v4/src/state/orderbook/mod.rs +++ b/programs/mango-v4/src/state/orderbook/mod.rs @@ -15,9 +15,7 @@ pub mod queue; #[cfg(test)] mod tests { use super::*; - use crate::state::{ - MangoAccount, MangoAccountValue, PerpMarket, FREE_ORDER_SLOT, QUOTE_TOKEN_INDEX, - }; + use crate::state::{MangoAccount, MangoAccountValue, PerpMarket, FREE_ORDER_SLOT}; use anchor_lang::prelude::*; use bytemuck::Zeroable; use fixed::types::I80F48; @@ -100,13 +98,14 @@ mod tests { bids: bids.borrow_mut(), asks: asks.borrow_mut(), }; + let settle_token_index = 0; let mut new_order = |book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 { let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); account - .ensure_perp_position(perp_market.perp_market_index, QUOTE_TOKEN_INDEX) + .ensure_perp_position(perp_market.perp_market_index, settle_token_index) .unwrap(); let quantity = 1; @@ -194,6 +193,7 @@ mod tests { bids: bids.borrow_mut(), asks: asks.borrow_mut(), }; + let settle_token_index = 0; // Add lots and fees to make sure to exercise unit conversion market.base_lot_size = 10; @@ -205,10 +205,10 @@ mod tests { let mut maker = MangoAccountValue::from_bytes(&buffer).unwrap(); let mut taker = MangoAccountValue::from_bytes(&buffer).unwrap(); maker - .ensure_perp_position(market.perp_market_index, QUOTE_TOKEN_INDEX) + .ensure_perp_position(market.perp_market_index, settle_token_index) .unwrap(); taker - .ensure_perp_position(market.perp_market_index, QUOTE_TOKEN_INDEX) + .ensure_perp_position(market.perp_market_index, settle_token_index) .unwrap(); let maker_pk = Pubkey::new_unique(); diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 12a4bb99b..3f7e85f2e 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -6,8 +6,8 @@ use fixed::types::I80F48; use static_assertions::const_assert_eq; use crate::accounts_zerocopy::KeyedAccountReader; -use crate::state::oracle; use crate::state::orderbook::order_type::Side; +use crate::state::{oracle, TokenIndex}; use crate::util::checked_math as cm; use super::{Book, OracleConfig, DAY_I80F48}; @@ -20,8 +20,7 @@ pub struct PerpMarket { // ABI: Clients rely on this being at offset 8 pub group: Pubkey, - // ABI: Clients rely on this being at offset 40 - pub padding0: [u8; 2], + pub settle_token_index: TokenIndex, /// Lookup indices pub perp_market_index: PerpMarketIndex, @@ -241,6 +240,7 @@ impl PerpMarket { pub fn default_for_tests() -> PerpMarket { PerpMarket { group: Pubkey::new_unique(), + settle_token_index: 0, perp_market_index: 0, name: Default::default(), oracle: Pubkey::new_unique(), @@ -272,7 +272,6 @@ impl PerpMarket { bump: 0, base_decimals: 0, reserved: [0; 92], - padding0: Default::default(), padding1: Default::default(), padding2: Default::default(), registration_time: 0, diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 94167d754..b4c015ada 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -2201,6 +2201,7 @@ pub struct PerpCreateMarketInstruction { pub bids: Pubkey, pub event_queue: Pubkey, pub payer: TestKeypair, + pub settle_token_index: TokenIndex, pub perp_market_index: PerpMarketIndex, pub base_decimals: u8, pub quote_lot_size: i64, @@ -2254,6 +2255,7 @@ impl ClientInstruction for PerpCreateMarketInstruction { oracle_config: OracleConfig { conf_filter: I80F48::from_num::(0.10), }, + settle_token_index: self.settle_token_index, perp_market_index: self.perp_market_index, quote_lot_size: self.quote_lot_size, base_lot_size: self.base_lot_size, @@ -2636,7 +2638,7 @@ pub struct PerpSettlePnlInstruction { pub account_a: Pubkey, pub account_b: Pubkey, pub perp_market: Pubkey, - pub quote_bank: Pubkey, + pub settle_bank: Pubkey, } #[async_trait::async_trait(?Send)] impl ClientInstruction for PerpSettlePnlInstruction { @@ -2650,6 +2652,7 @@ impl ClientInstruction for PerpSettlePnlInstruction { let instruction = Self::Instruction {}; let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap(); + let settle_bank: Bank = account_loader.load(&self.settle_bank).await.unwrap(); let account_a = account_loader .load_mango_account(&self.account_a) .await @@ -2677,7 +2680,8 @@ impl ClientInstruction for PerpSettlePnlInstruction { account_a: self.account_a, account_b: self.account_b, oracle: perp_market.oracle, - quote_bank: self.quote_bank, + settle_bank: self.settle_bank, + settle_oracle: settle_bank.oracle, }; let mut instruction = make_instruction(program_id, &accounts, instruction); @@ -2694,7 +2698,7 @@ impl ClientInstruction for PerpSettlePnlInstruction { pub struct PerpSettleFeesInstruction { pub account: Pubkey, pub perp_market: Pubkey, - pub quote_bank: Pubkey, + pub settle_bank: Pubkey, pub max_settle_amount: u64, } #[async_trait::async_trait(?Send)] @@ -2711,6 +2715,7 @@ impl ClientInstruction for PerpSettleFeesInstruction { }; let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap(); + let settle_bank: Bank = account_loader.load(&self.settle_bank).await.unwrap(); let account = account_loader .load_mango_account(&self.account) .await @@ -2729,7 +2734,8 @@ impl ClientInstruction for PerpSettleFeesInstruction { perp_market: self.perp_market, account: self.account, oracle: perp_market.oracle, - quote_bank: self.quote_bank, + settle_bank: self.settle_bank, + settle_oracle: settle_bank.oracle, }; let mut instruction = make_instruction(program_id, &accounts, instruction); instruction.accounts.extend(health_check_metas); @@ -2908,8 +2914,9 @@ impl ClientInstruction for PerpLiqBankruptcyInstruction { liqor: self.liqor, liqor_owner: self.liqor_owner.pubkey(), liqee: self.liqee, - quote_bank: quote_mint_info.first_bank(), - quote_vault: quote_mint_info.first_vault(), + settle_bank: quote_mint_info.first_bank(), + settle_vault: quote_mint_info.first_vault(), + settle_oracle: quote_mint_info.oracle, insurance_vault: group.insurance_vault, token_program: Token::id(), }; diff --git a/programs/mango-v4/tests/test_liq_perps.rs b/programs/mango-v4/tests/test_liq_perps.rs index 2275c1f57..5fddfd434 100644 --- a/programs/mango-v4/tests/test_liq_perps.rs +++ b/programs/mango-v4/tests/test_liq_perps.rs @@ -522,7 +522,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr account_a: liqor, account_b: account_1, perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, }, ) .await diff --git a/programs/mango-v4/tests/test_perp.rs b/programs/mango-v4/tests/test_perp.rs index 975f6afea..d3b956cfa 100644 --- a/programs/mango-v4/tests/test_perp.rs +++ b/programs/mango-v4/tests/test_perp.rs @@ -392,7 +392,7 @@ async fn test_perp() -> Result<(), TransportError> { account_a: account_0, account_b: account_1, perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, }, ) .await @@ -402,7 +402,7 @@ async fn test_perp() -> Result<(), TransportError> { PerpSettleFeesInstruction { account: account_1, perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, max_settle_amount: u64::MAX, }, ) diff --git a/programs/mango-v4/tests/test_perp_settle.rs b/programs/mango-v4/tests/test_perp_settle.rs index 079c50a6e..24e435479 100644 --- a/programs/mango-v4/tests/test_perp_settle.rs +++ b/programs/mango-v4/tests/test_perp_settle.rs @@ -200,7 +200,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { account_a: account_1, account_b: account_0, perp_market, - quote_bank: tokens[1].bank, + settle_bank: tokens[1].bank, }, ) .await; @@ -220,7 +220,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { account_a: account_0, account_b: account_0, perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, }, ) .await; @@ -240,7 +240,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { account_a: account_0, account_b: account_1, perp_market: perp_market_2, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, }, ) .await; @@ -292,7 +292,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { account_a: account_1, account_b: account_0, perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, }, ) .await; @@ -376,7 +376,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { account_a: account_0, account_b: account_1, perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, }, ) .await @@ -471,7 +471,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { account_a: account_1, account_b: account_0, perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, }, ) .await @@ -569,7 +569,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> { } .create(solana) .await; - let quote_bank = tokens[0].bank; + let settle_bank = tokens[0].bank; // ensure vaults are not empty create_funded_account( @@ -744,7 +744,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> { account_a: account_0, account_b: account_1, perp_market, - quote_bank, + settle_bank, }, ) .await @@ -764,15 +764,15 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> { I80F48::from(100_000 + total_settled_pnl), ); assert_eq!( - account_position(solana, account_0, quote_bank).await, + account_position(solana, account_0, settle_bank).await, initial_token_deposit as i64 + total_settled_pnl - total_fees_paid ); assert_eq!( - account_position(solana, account_1, quote_bank).await, + account_position(solana, account_1, settle_bank).await, initial_token_deposit as i64 - total_settled_pnl ); assert_eq!( - account_position(solana, settler, quote_bank).await, + account_position(solana, settler, settle_bank).await, total_fees_paid ); } @@ -834,7 +834,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> { account_a: account_0, account_b: account_1, perp_market, - quote_bank, + settle_bank, }, ) .await @@ -856,15 +856,15 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> { I80F48::from(100_000 + total_settled_pnl), ); assert_eq!( - account_position(solana, account_0, quote_bank).await, + account_position(solana, account_0, settle_bank).await, initial_token_deposit as i64 + total_settled_pnl - total_fees_paid ); assert_eq!( - account_position(solana, account_1, quote_bank).await, + account_position(solana, account_1, settle_bank).await, initial_token_deposit as i64 - total_settled_pnl ); assert_eq!( - account_position(solana, settler, quote_bank).await, + account_position(solana, settler, settle_bank).await, total_fees_paid ); } diff --git a/programs/mango-v4/tests/test_perp_settle_fees.rs b/programs/mango-v4/tests/test_perp_settle_fees.rs index 7359078bf..116b2a74a 100644 --- a/programs/mango-v4/tests/test_perp_settle_fees.rs +++ b/programs/mango-v4/tests/test_perp_settle_fees.rs @@ -270,7 +270,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { PerpSettleFeesInstruction { account: account_0, perp_market, - quote_bank: tokens[1].bank, + settle_bank: tokens[1].bank, max_settle_amount: u64::MAX, }, ) @@ -288,7 +288,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { PerpSettleFeesInstruction { account: account_1, perp_market: perp_market_2, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, max_settle_amount: u64::MAX, }, ) @@ -306,7 +306,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { PerpSettleFeesInstruction { account: account_1, perp_market: perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, max_settle_amount: 0, }, ) @@ -354,7 +354,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { PerpSettleFeesInstruction { account: account_0, perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, max_settle_amount: u64::MAX, }, ) @@ -374,7 +374,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { // account: account_1, // perp_market, // oracle: tokens[0].oracle, - // quote_bank: tokens[0].bank, + // settle_bank: tokens[0].bank, // max_settle_amount: I80F48::MAX, // }, // ) @@ -438,7 +438,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { PerpSettleFeesInstruction { account: account_1, perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, max_settle_amount: partial_settle_amount, }, ) @@ -492,7 +492,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { PerpSettleFeesInstruction { account: account_1, perp_market, - quote_bank: tokens[0].bank, + settle_bank: tokens[0].bank, max_settle_amount: u64::MAX, }, ) diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 01995095d..49e605426 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1327,7 +1327,6 @@ export class MangoClient { name: string, oracleConfFilter: number, baseDecimals: number, - quoteTokenIndex: number, quoteLotSize: number, baseLotSize: number, maintAssetWeight: number, @@ -1346,6 +1345,7 @@ export class MangoClient { settleFeeFlat: number, settleFeeAmountThreshold: number, settleFeeFractionLowHealth: number, + settleTokenIndex: number, ): Promise { const bids = new Keypair(); const asks = new Keypair(); @@ -1386,6 +1386,7 @@ export class MangoClient { settleFeeFlat, settleFeeAmountThreshold, settleFeeFractionLowHealth, + settleTokenIndex, ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 9292bb01c..c4f2ec621 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -2387,6 +2387,10 @@ export type MangoV4 = { { "name": "settleFeeFractionLowHealth", "type": "f32" + }, + { + "name": "settleTokenIndex", + "type": "u16" } ] }, @@ -2951,9 +2955,14 @@ export type MangoV4 = { "isSigner": false }, { - "name": "quoteBank", + "name": "settleBank", "isMut": true, "isSigner": false + }, + { + "name": "settleOracle", + "isMut": false, + "isSigner": false } ], "args": [] @@ -2982,9 +2991,14 @@ export type MangoV4 = { "isSigner": false }, { - "name": "quoteBank", + "name": "settleBank", "isMut": true, "isSigner": false + }, + { + "name": "settleOracle", + "isMut": false, + "isSigner": false } ], "args": [ @@ -3105,15 +3119,20 @@ export type MangoV4 = { "isSigner": false }, { - "name": "quoteBank", + "name": "settleBank", "isMut": true, "isSigner": false }, { - "name": "quoteVault", + "name": "settleVault", "isMut": true, "isSigner": false }, + { + "name": "settleOracle", + "isMut": false, + "isSigner": false + }, { "name": "insuranceVault", "isMut": true, @@ -3896,13 +3915,8 @@ export type MangoV4 = { "type": "publicKey" }, { - "name": "padding0", - "type": { - "array": [ - "u8", - 2 - ] - } + "name": "settleTokenIndex", + "type": "u16" }, { "name": "perpMarketIndex", @@ -8649,6 +8663,10 @@ export const IDL: MangoV4 = { { "name": "settleFeeFractionLowHealth", "type": "f32" + }, + { + "name": "settleTokenIndex", + "type": "u16" } ] }, @@ -9213,9 +9231,14 @@ export const IDL: MangoV4 = { "isSigner": false }, { - "name": "quoteBank", + "name": "settleBank", "isMut": true, "isSigner": false + }, + { + "name": "settleOracle", + "isMut": false, + "isSigner": false } ], "args": [] @@ -9244,9 +9267,14 @@ export const IDL: MangoV4 = { "isSigner": false }, { - "name": "quoteBank", + "name": "settleBank", "isMut": true, "isSigner": false + }, + { + "name": "settleOracle", + "isMut": false, + "isSigner": false } ], "args": [ @@ -9367,15 +9395,20 @@ export const IDL: MangoV4 = { "isSigner": false }, { - "name": "quoteBank", + "name": "settleBank", "isMut": true, "isSigner": false }, { - "name": "quoteVault", + "name": "settleVault", "isMut": true, "isSigner": false }, + { + "name": "settleOracle", + "isMut": false, + "isSigner": false + }, { "name": "insuranceVault", "isMut": true, @@ -10158,13 +10191,8 @@ export const IDL: MangoV4 = { "type": "publicKey" }, { - "name": "padding0", - "type": { - "array": [ - "u8", - 2 - ] - } + "name": "settleTokenIndex", + "type": "u16" }, { "name": "perpMarketIndex", diff --git a/ts/client/src/scripts/devnet-admin.ts b/ts/client/src/scripts/devnet-admin.ts index d706407f9..a1f977514 100644 --- a/ts/client/src/scripts/devnet-admin.ts +++ b/ts/client/src/scripts/devnet-admin.ts @@ -364,7 +364,6 @@ async function main() { 'BTC-PERP', 0.1, 6, - 1, 10, 100, 0.975, @@ -383,6 +382,7 @@ async function main() { 1000, 1000000, 0.05, + 0, ); console.log('done'); } catch (error) { diff --git a/ts/client/src/scripts/mb-liqtest-create-group.ts b/ts/client/src/scripts/mb-liqtest-create-group.ts index 28c7a0f18..838db37e2 100644 --- a/ts/client/src/scripts/mb-liqtest-create-group.ts +++ b/ts/client/src/scripts/mb-liqtest-create-group.ts @@ -204,7 +204,6 @@ async function main() { 'MNGO-PERP', 0.1, 9, - 0, 10, 100000, // base lots 0.9, @@ -223,6 +222,7 @@ async function main() { 0, 0, 0, + 0, ); } catch (error) { console.log(error); From 6808171ee3f22f70703ee8c13743869f16cdfa8d Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Thu, 6 Oct 2022 11:25:09 +0200 Subject: [PATCH 37/39] ts: disable versioned txs for now because wallets dont support them Signed-off-by: microwavedcola1 --- ts/client/src/utils/rpc.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/ts/client/src/utils/rpc.ts b/ts/client/src/utils/rpc.ts index e8a7e9b3c..8c01ea8a5 100644 --- a/ts/client/src/utils/rpc.ts +++ b/ts/client/src/utils/rpc.ts @@ -1,6 +1,7 @@ import { AnchorProvider } from '@project-serum/anchor'; import { AddressLookupTableAccount, + Transaction, TransactionInstruction, } from '@solana/web3.js'; import { buildVersionedTx } from '../utils'; @@ -15,12 +16,23 @@ export async function sendTransaction( const latestBlockhash = await connection.getLatestBlockhash( opts.preflightCommitment, ); - const tx = await buildVersionedTx( - provider, - ixs, - opts.additionalSigners, - alts, - ); + + let tx: Transaction = new Transaction(); + const altsEnabled = false; + if (altsEnabled) { + tx = await buildVersionedTx(provider, ixs, opts.additionalSigners, alts); + } else { + const payer = (provider as AnchorProvider).wallet; + tx = new Transaction(); + tx.recentBlockhash = latestBlockhash.blockhash; + tx.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight; + tx.feePayer = payer.publicKey; + tx.add(...ixs); + if (opts.additionalSigners?.length > 0) { + tx.partialSign(...opts.additionalSigners); + } + await payer.signTransaction(tx); + } const signature = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true, From 8e919bb741b0744bebce42d1fcafaab248017752 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Fri, 7 Oct 2022 13:52:04 +0200 Subject: [PATCH 38/39] ts: perp improvements (#263) * ts: perp improvements Signed-off-by: microwavedcola1 * ts: fixes from review Signed-off-by: microwavedcola1 * ts: fixes from review Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- ts/client/src/accounts/healthCache.ts | 203 ++++--- ts/client/src/accounts/mangoAccount.ts | 98 +++- ts/client/src/accounts/perp.ts | 29 +- ts/client/src/client.ts | 140 +++-- ts/client/src/scripts/devnet-admin.ts | 2 +- ts/client/src/scripts/devnet-user.ts | 326 ++++++----- ts/client/src/scripts/mm/log-perp-ob.ts | 48 ++ ts/client/src/scripts/mm/market-maker.ts | 514 ++++++++++++++++++ ts/client/src/scripts/mm/params/default.json | 16 + .../src/scripts/mm/sequence-enforcer-util.ts | 68 +++ ts/client/src/scripts/mm/taker.ts | 111 ++++ ts/client/src/utils/rpc.ts | 2 + 12 files changed, 1287 insertions(+), 270 deletions(-) create mode 100644 ts/client/src/scripts/mm/log-perp-ob.ts create mode 100644 ts/client/src/scripts/mm/market-maker.ts create mode 100644 ts/client/src/scripts/mm/params/default.json create mode 100644 ts/client/src/scripts/mm/sequence-enforcer-util.ts create mode 100644 ts/client/src/scripts/mm/taker.ts diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index e3dc93af8..5b9c95e9f 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -239,6 +239,27 @@ export class HealthCache { return this.findTokenInfoIndex(bank.tokenIndex); } + simHealthRatioWithTokenPositionChanges( + group: Group, + nativeTokenChanges: { + nativeTokenAmount: I80F48; + mintPk: PublicKey; + }[], + healthType: HealthType = HealthType.init, + ): I80F48 { + const adjustedCache: HealthCache = _.cloneDeep(this); + // HealthCache.logHealthCache('beforeChange', adjustedCache); + for (const change of nativeTokenChanges) { + const bank: Bank = group.getFirstBankByMint(change.mintPk); + const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank); + adjustedCache.tokenInfos[changeIndex].balance.iadd( + change.nativeTokenAmount.mul(bank.price), + ); + } + // HealthCache.logHealthCache('afterChange', adjustedCache); + return adjustedCache.healthRatio(healthType); + } + findSerum3InfoIndex(marketIndex: MarketIndex): number { return this.serum3Infos.findIndex( (serum3Info) => serum3Info.marketIndex === marketIndex, @@ -266,7 +287,6 @@ export class HealthCache { } adjustSerum3Reserved( - // todo change indices to types from numbers baseBank: BankForHealth, quoteBank: BankForHealth, serum3Market: Serum3Market, @@ -300,64 +320,6 @@ export class HealthCache { serum3Info.reserved = serum3Info.reserved.add(reservedAmount); } - findPerpInfoIndex(perpMarketIndex: number): number { - return this.perpInfos.findIndex( - (perpInfo) => perpInfo.perpMarketIndex === perpMarketIndex, - ); - } - - getOrCreatePerpInfoIndex(perpMarket: PerpMarket): number { - const index = this.findPerpInfoIndex(perpMarket.perpMarketIndex); - if (index == -1) { - this.perpInfos.push(PerpInfo.emptyFromPerpMarket(perpMarket)); - } - return this.findPerpInfoIndex(perpMarket.perpMarketIndex); - } - - public static logHealthCache(debug: string, healthCache: HealthCache): void { - if (debug) console.log(debug); - for (const token of healthCache.tokenInfos) { - console.log(` ${token.toString()}`); - } - for (const serum3Info of healthCache.serum3Infos) { - console.log(` ${serum3Info.toString(healthCache.tokenInfos)}`); - } - console.log( - ` assets ${healthCache.assets( - HealthType.init, - )}, liabs ${healthCache.liabs(HealthType.init)}, `, - ); - console.log( - ` health(HealthType.init) ${healthCache.health(HealthType.init)}`, - ); - console.log( - ` healthRatio(HealthType.init) ${healthCache.healthRatio( - HealthType.init, - )}`, - ); - } - - simHealthRatioWithTokenPositionChanges( - group: Group, - nativeTokenChanges: { - nativeTokenAmount: I80F48; - mintPk: PublicKey; - }[], - healthType: HealthType = HealthType.init, - ): I80F48 { - const adjustedCache: HealthCache = _.cloneDeep(this); - // HealthCache.logHealthCache('beforeChange', adjustedCache); - for (const change of nativeTokenChanges) { - const bank: Bank = group.getFirstBankByMint(change.mintPk); - const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank); - adjustedCache.tokenInfos[changeIndex].balance.iadd( - change.nativeTokenAmount.mul(bank.price), - ); - } - // HealthCache.logHealthCache('afterChange', adjustedCache); - return adjustedCache.healthRatio(healthType); - } - simHealthRatioWithSerum3BidChanges( baseBank: BankForHealth, quoteBank: BankForHealth, @@ -422,6 +384,83 @@ export class HealthCache { return adjustedCache.healthRatio(healthType); } + findPerpInfoIndex(perpMarketIndex: number): number { + return this.perpInfos.findIndex( + (perpInfo) => perpInfo.perpMarketIndex === perpMarketIndex, + ); + } + + getOrCreatePerpInfoIndex(perpMarket: PerpMarket): number { + const index = this.findPerpInfoIndex(perpMarket.perpMarketIndex); + if (index == -1) { + this.perpInfos.push(PerpInfo.emptyFromPerpMarket(perpMarket)); + } + return this.findPerpInfoIndex(perpMarket.perpMarketIndex); + } + + recomputePerpInfo( + perpMarket: PerpMarket, + perpInfoIndex: number, + clonedExistingPerpPosition: PerpPosition, + side: PerpOrderSide, + newOrderBaseLots: BN, + ): void { + if (side == PerpOrderSide.bid) { + clonedExistingPerpPosition.bidsBaseLots.iadd(newOrderBaseLots); + } else { + clonedExistingPerpPosition.asksBaseLots.iadd(newOrderBaseLots); + } + this.perpInfos[perpInfoIndex] = PerpInfo.fromPerpPosition( + perpMarket, + clonedExistingPerpPosition, + ); + } + + simHealthRatioWithPerpOrderChanges( + perpMarket: PerpMarket, + existingPerpPosition: PerpPosition, + baseLots: BN, + side: PerpOrderSide, + healthType: HealthType = HealthType.init, + ): I80F48 { + const clonedHealthCache: HealthCache = _.cloneDeep(this); + const clonedExistingPosition: PerpPosition = + _.cloneDeep(existingPerpPosition); + const perpInfoIndex = + clonedHealthCache.getOrCreatePerpInfoIndex(perpMarket); + clonedHealthCache.recomputePerpInfo( + perpMarket, + perpInfoIndex, + clonedExistingPosition, + side, + baseLots, + ); + return clonedHealthCache.healthRatio(healthType); + } + + public static logHealthCache(debug: string, healthCache: HealthCache): void { + if (debug) console.log(debug); + for (const token of healthCache.tokenInfos) { + console.log(` ${token.toString()}`); + } + for (const serum3Info of healthCache.serum3Infos) { + console.log(` ${serum3Info.toString(healthCache.tokenInfos)}`); + } + console.log( + ` assets ${healthCache.assets( + HealthType.init, + )}, liabs ${healthCache.liabs(HealthType.init)}, `, + ); + console.log( + ` health(HealthType.init) ${healthCache.health(HealthType.init)}`, + ); + console.log( + ` healthRatio(HealthType.init) ${healthCache.healthRatio( + HealthType.init, + )}`, + ); + } + private static binaryApproximationSearch( left: I80F48, leftRatio: I80F48, @@ -719,9 +758,9 @@ export class HealthCache { getMaxPerpForHealthRatio( perpMarket: PerpMarket, + existingPerpPosition: PerpPosition, side: PerpOrderSide, minRatio: I80F48, - price: I80F48, ): I80F48 { const healthCacheClone: HealthCache = _.cloneDeep(this); @@ -732,37 +771,43 @@ export class HealthCache { const direction = side == PerpOrderSide.bid ? 1 : -1; - const perpInfoIndex = this.getOrCreatePerpInfoIndex(perpMarket); - const perpInfo = this.perpInfos[perpInfoIndex]; + const perpInfoIndex = healthCacheClone.getOrCreatePerpInfoIndex(perpMarket); + const perpInfo = healthCacheClone.perpInfos[perpInfoIndex]; const oraclePrice = perpInfo.oraclePrice; const baseLotSize = I80F48.fromI64(perpMarket.baseLotSize); // If the price is sufficiently good then health will just increase from trading const finalHealthSlope = direction == 1 - ? perpInfo.initAssetWeight.mul(oraclePrice).sub(price) - : price.sub(perpInfo.initLiabWeight.mul(oraclePrice)); + ? perpInfo.initAssetWeight.mul(oraclePrice).sub(oraclePrice) + : oraclePrice.sub(perpInfo.initLiabWeight.mul(oraclePrice)); if (finalHealthSlope.gte(ZERO_I80F48())) { return MAX_I80F48(); } - function cacheAfterTrade(baseLots: I80F48): HealthCache { + function cacheAfterPlaceOrder(baseLots: BN): HealthCache { const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); - const d = I80F48.fromNumber(direction); - adjustedCache.perpInfos[perpInfoIndex].base.iadd( - d.mul(baseLots.mul(baseLotSize.mul(oraclePrice))), - ); - adjustedCache.perpInfos[perpInfoIndex].quote.isub( - d.mul(baseLots.mul(baseLotSize.mul(price))), + const adjustedExistingPerpPosition: PerpPosition = + _.cloneDeep(existingPerpPosition); + adjustedCache.recomputePerpInfo( + perpMarket, + perpInfoIndex, + adjustedExistingPerpPosition, + side, + baseLots, ); return adjustedCache; } function healthAfterTrade(baseLots: I80F48): I80F48 { - return cacheAfterTrade(baseLots).health(HealthType.init); + return cacheAfterPlaceOrder(new BN(baseLots.toNumber())).health( + HealthType.init, + ); } function healthRatioAfterTrade(baseLots: I80F48): I80F48 { - return cacheAfterTrade(baseLots).healthRatio(HealthType.init); + return cacheAfterPlaceOrder(new BN(baseLots.toNumber())).healthRatio( + HealthType.init, + ); } const initialBaseLots = perpInfo.base @@ -1064,7 +1109,7 @@ export class PerpInfo { perpPosition.basePositionLots.add(perpPosition.takerBaseLots), ); - const unsettledFunding = perpPosition.unsettledFunding(perpMarket); + const unsettledFunding = perpPosition.getUnsettledFunding(perpMarket); const takerQuote = I80F48.fromI64( new BN(perpPosition.takerQuoteLots).mul(perpMarket.quoteLotSize), @@ -1164,13 +1209,13 @@ export class PerpInfo { weight = this.maintAssetWeight; } - // console.log(`initLiabWeight ${this.initLiabWeight}`); - // console.log(`initAssetWeight ${this.initAssetWeight}`); - // console.log(`weight ${weight}`); - // console.log(`this.quote ${this.quote}`); - // console.log(`this.base ${this.base}`); + // console.log(` - this.quote ${this.quote}`); + // console.log(` - weight ${weight}`); + // console.log(` - this.base ${this.base}`); + // console.log(` - weight.mul(this.base) ${weight.mul(this.base)}`); const uncappedHealthContribution = this.quote.add(weight.mul(this.base)); + // console.log(` - uncappedHealthContribution ${uncappedHealthContribution}`); if (this.trustedMarket) { return uncappedHealthContribution; } else { diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 94aaab350..ee70ef3ff 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -153,6 +153,19 @@ export class MangoAccount { return this.serum3.find((sa) => sa.marketIndex == marketIndex); } + getPerpPosition(perpMarketIndex: PerpMarketIndex): PerpPosition | undefined { + return this.perps.find((pp) => pp.marketIndex == perpMarketIndex); + } + + getPerpPositionUi(group: Group, perpMarketIndex: PerpMarketIndex): number { + const pp = this.perps.find((pp) => pp.marketIndex == perpMarketIndex); + if (!pp) { + throw new Error(`No position found for PerpMarket ${perpMarketIndex}!`); + } + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); + return pp.getBasePositionUi(perpMarket); + } + getSerum3OoAccount(marketIndex: MarketIndex): OpenOrders { const oo: OpenOrders | undefined = this.serum3OosMapByMarketIndex.get(marketIndex); @@ -543,7 +556,7 @@ export class MangoAccount { /** * @param group * @param externalMarketPk - * @returns maximum ui quote which can be traded for base token given current health + * @returns maximum ui quote which can be traded at oracle price for base token given current health */ public getMaxQuoteForSerum3BidUi( group: Group, @@ -580,7 +593,7 @@ export class MangoAccount { /** * @param group * @param externalMarketPk - * @returns maximum ui base which can be traded for quote token given current health + * @returns maximum ui base which can be traded at oracle price for quote token given current health */ public getMaxBaseForSerum3AskUi( group: Group, @@ -694,21 +707,22 @@ export class MangoAccount { * * @param group * @param perpMarketName - * @param uiPrice ui price at which bid would be placed at - * @returns max ui quote bid + * @returns maximum ui quote which can be traded at oracle price for quote token given current health */ public getMaxQuoteForPerpBidUi( group: Group, perpMarketIndex: PerpMarketIndex, - uiPrice: number, ): number { const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); + const pp = this.getPerpPosition(perpMarket.perpMarketIndex); const hc = HealthCache.fromMangoAccount(group, this); const baseLots = hc.getMaxPerpForHealthRatio( perpMarket, + pp + ? pp + : PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex), PerpOrderSide.bid, I80F48.fromNumber(2), - group.toNativePrice(uiPrice, perpMarket.baseDecimals), ); const nativeBase = baseLots.mul(I80F48.fromI64(perpMarket.baseLotSize)); const nativeQuote = nativeBase.mul(perpMarket.price); @@ -725,19 +739,63 @@ export class MangoAccount { public getMaxBaseForPerpAskUi( group: Group, perpMarketIndex: PerpMarketIndex, - uiPrice: number, ): number { const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); + const pp = this.getPerpPosition(perpMarket.perpMarketIndex); const hc = HealthCache.fromMangoAccount(group, this); const baseLots = hc.getMaxPerpForHealthRatio( perpMarket, + pp + ? pp + : PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex), PerpOrderSide.ask, I80F48.fromNumber(2), - group.toNativePrice(uiPrice, perpMarket.baseDecimals), ); return perpMarket.baseLotsToUi(new BN(baseLots.toString())); } + public simHealthRatioWithPerpBidUiChanges( + group: Group, + perpMarketIndex: PerpMarketIndex, + size: number, + ): number { + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); + const pp = this.getPerpPosition(perpMarket.perpMarketIndex); + const hc = HealthCache.fromMangoAccount(group, this); + return hc + .simHealthRatioWithPerpOrderChanges( + perpMarket, + pp + ? pp + : PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex), + perpMarket.uiBaseToLots(size), + PerpOrderSide.bid, + HealthType.init, + ) + .toNumber(); + } + + public simHealthRatioWithPerpAskUiChanges( + group: Group, + perpMarketIndex: PerpMarketIndex, + size: number, + ): number { + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); + const pp = this.getPerpPosition(perpMarket.perpMarketIndex); + const hc = HealthCache.fromMangoAccount(group, this); + return hc + .simHealthRatioWithPerpOrderChanges( + perpMarket, + pp + ? pp + : PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex), + perpMarket.uiBaseToLots(size), + PerpOrderSide.ask, + HealthType.init, + ) + .toNumber(); + } + public async loadPerpOpenOrdersForMarket( client: MangoClient, group: Group, @@ -963,6 +1021,22 @@ export class PerpPosition { ); } + static emptyFromPerpMarketIndex( + perpMarketIndex: PerpMarketIndex, + ): PerpPosition { + return new PerpPosition( + perpMarketIndex, + new BN(0), + ZERO_I80F48(), + new BN(0), + new BN(0), + new BN(0), + new BN(0), + ZERO_I80F48(), + ZERO_I80F48(), + ); + } + constructor( public marketIndex: PerpMarketIndex, public basePositionLots: BN, @@ -979,7 +1053,11 @@ export class PerpPosition { return this.marketIndex != PerpPosition.PerpMarketIndexUnset; } - public unsettledFunding(perpMarket: PerpMarket): I80F48 { + public getBasePositionUi(perpMarket: PerpMarket): number { + return perpMarket.baseLotsToUi(this.basePositionLots); + } + + public getUnsettledFunding(perpMarket: PerpMarket): I80F48 { if (this.basePositionLots.gt(new BN(0))) { return perpMarket.longFunding .sub(this.longSettledFunding) @@ -1001,7 +1079,7 @@ export class PerpPosition { this.basePositionLots.add(this.takerBaseLots), ); - const unsettledFunding = this.unsettledFunding(perpMarket); + const unsettledFunding = this.getUnsettledFunding(perpMarket); const takerQuote = I80F48.fromI64( new BN(this.takerQuoteLots).mul(perpMarket.quoteLotSize), ); diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index 986b79918..afc1b6cef 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -209,6 +209,25 @@ export class PerpMarket { .filter((event) => event.eventType == PerpEventQueue.FILL_EVENT_TYPE); } + public async logOb(client: MangoClient): Promise { + let res = ``; + res += ` ${this.name} OrderBook`; + let orders = await this?.loadAsks(client); + for (const order of orders!.items()) { + res += `\n ${order.uiPrice.toFixed(5).padStart(10)}, ${order.uiSize + .toString() + .padStart(10)}`; + } + res += `\n asks ↑ --------- ↓ bids`; + orders = await this?.loadBids(client); + for (const order of orders!.items()) { + res += `\n ${order.uiPrice.toFixed(5).padStart(10)}, ${order.uiSize + .toString() + .padStart(10)}`; + } + return res; + } + /** * * @param bids @@ -380,6 +399,10 @@ export class BookSide { } } + public best(): PerpOrder | undefined { + return this.items().next().value; + } + getImpactPriceUi(baseLots: BN): number | undefined { const s = new BN(0); for (const order of this.items()) { @@ -491,10 +514,10 @@ export class PerpOrderSide { export class PerpOrderType { static limit = { limit: {} }; - static immediateOrCancel = { immediateorcancel: {} }; - static postOnly = { postonly: {} }; + static immediateOrCancel = { immediateOrCancel: {} }; + static postOnly = { postOnly: {} }; static market = { market: {} }; - static postOnlySlide = { postonlyslide: {} }; + static postOnlySlide = { postOnlySlide: {} }; } export class PerpOrder { diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 49e605426..068d1afda 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -697,6 +697,30 @@ export class MangoClient { }); } + public async getMangoAccountsForDelegate( + group: Group, + delegate: PublicKey, + ): Promise { + return ( + await this.program.account.mangoAccount.all([ + { + memcmp: { + bytes: group.publicKey.toBase58(), + offset: 8, + }, + }, + { + memcmp: { + bytes: delegate.toBase58(), + offset: 104, + }, + }, + ]) + ).map((pa) => { + return MangoAccount.from(pa.publicKey, pa.account); + }); + } + public async getAllMangoAccounts(group: Group): Promise { return ( await this.program.account.mangoAccount.all([ @@ -1183,7 +1207,7 @@ export class MangoClient { ); } - async serum3CancelAllorders( + public async serum3CancelAllOrders( group: Group, mangoAccount: MangoAccount, externalMarketPk: PublicKey, @@ -1224,7 +1248,7 @@ export class MangoClient { ); } - async serum3SettleFunds( + public async serum3SettleFunds( group: Group, mangoAccount: MangoAccount, externalMarketPk: PublicKey, @@ -1277,7 +1301,7 @@ export class MangoClient { ); } - async serum3CancelOrder( + public async serum3CancelOrder( group: Group, mangoAccount: MangoAccount, externalMarketPk: PublicKey, @@ -1320,7 +1344,7 @@ export class MangoClient { /// perps - async perpCreateMarket( + public async perpCreateMarket( group: Group, oraclePk: PublicKey, perpMarketIndex: number, @@ -1438,7 +1462,7 @@ export class MangoClient { .rpc(); } - async perpEditMarket( + public async perpEditMarket( group: Group, perpMarketIndex: PerpMarketIndex, oracle: PublicKey, @@ -1497,7 +1521,7 @@ export class MangoClient { .rpc(); } - async perpCloseMarket( + public async perpCloseMarket( group: Group, perpMarketIndex: PerpMarketIndex, ): Promise { @@ -1536,7 +1560,7 @@ export class MangoClient { ); } - async perpDeactivatePosition( + public async perpDeactivatePosition( group: Group, mangoAccount: MangoAccount, perpMarketIndex: PerpMarketIndex, @@ -1567,19 +1591,56 @@ export class MangoClient { .rpc(); } - async perpPlaceOrder( + public async perpPlaceOrder( group: Group, mangoAccount: MangoAccount, perpMarketIndex: PerpMarketIndex, side: PerpOrderSide, price: number, quantity: number, - maxQuoteQuantity: number, - clientOrderId: number, - orderType: PerpOrderType, - expiryTimestamp: number, - limit: number, + maxQuoteQuantity: number | undefined, + clientOrderId: number | undefined, + orderType: PerpOrderType | undefined, + expiryTimestamp: number | undefined, + limit: number | undefined, ): Promise { + return await sendTransaction( + this.program.provider as AnchorProvider, + [ + await this.perpPlaceOrderIx( + group, + mangoAccount, + perpMarketIndex, + side, + price, + quantity, + maxQuoteQuantity, + clientOrderId, + orderType, + expiryTimestamp, + limit, + ), + ], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); + } + + public async perpPlaceOrderIx( + group: Group, + mangoAccount: MangoAccount, + perpMarketIndex: PerpMarketIndex, + side: PerpOrderSide, + price: number, + quantity: number, + maxQuoteQuantity?: number, + clientOrderId?: number, + orderType?: PerpOrderType, + expiryTimestamp?: number, + limit?: number, + ): Promise { const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const healthRemainingAccounts: PublicKey[] = this.buildHealthRemainingAccounts( @@ -1590,7 +1651,7 @@ export class MangoClient { [group.getFirstBankByTokenIndex(0 as TokenIndex)], [perpMarket], ); - const ix = await this.program.methods + return await this.program.methods .perpPlaceOrder( side, perpMarket.uiPriceToLots(price), @@ -1598,10 +1659,10 @@ export class MangoClient { maxQuoteQuantity ? perpMarket.uiQuoteToLots(maxQuoteQuantity) : I64_MAX_BN, - new BN(clientOrderId), - orderType, - new BN(expiryTimestamp), - limit, + new BN(clientOrderId ? clientOrderId : Date.now()), + orderType ? orderType : PerpOrderType.limit, + new BN(expiryTimestamp ? expiryTimestamp : 0), + limit ? limit : 10, ) .accounts({ group: group.publicKey, @@ -1620,10 +1681,24 @@ export class MangoClient { ), ) .instruction(); + } + public async perpCancelAllOrders( + group: Group, + mangoAccount: MangoAccount, + perpMarketIndex: PerpMarketIndex, + limit: number, + ): Promise { return await sendTransaction( this.program.provider as AnchorProvider, - [ix], + [ + await this.perpCancelAllOrdersIx( + group, + mangoAccount, + perpMarketIndex, + limit, + ), + ], group.addressLookupTablesList, { postSendTxCallback: this.postSendTxCallback, @@ -1631,14 +1706,14 @@ export class MangoClient { ); } - async perpCancelAllOrders( + public async perpCancelAllOrdersIx( group: Group, mangoAccount: MangoAccount, perpMarketIndex: PerpMarketIndex, limit: number, - ): Promise { + ): Promise { const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); - const ix = await this.program.methods + return await this.program.methods .perpCancelAllOrders(limit) .accounts({ group: group.publicKey, @@ -1649,18 +1724,9 @@ export class MangoClient { owner: (this.program.provider as AnchorProvider).wallet.publicKey, }) .instruction(); - - return await sendTransaction( - this.program.provider as AnchorProvider, - [ix], - group.addressLookupTablesList, - { - postSendTxCallback: this.postSendTxCallback, - }, - ); } - async perpConsumeEvents( + public async perpConsumeEvents( group: Group, perpMarketIndex: PerpMarketIndex, accounts: PublicKey[], @@ -1683,7 +1749,7 @@ export class MangoClient { .rpc(); } - async perpConsumeAllEvents( + public async perpConsumeAllEvents( group: Group, perpMarketIndex: PerpMarketIndex, ): Promise { @@ -1887,7 +1953,7 @@ export class MangoClient { ); } - async updateIndexAndRate( + public async updateIndexAndRate( group: Group, mintPk: PublicKey, ): Promise { @@ -1915,7 +1981,7 @@ export class MangoClient { /// liquidations - async liqTokenWithToken( + public async liqTokenWithToken( group: Group, liqor: MangoAccount, liqee: MangoAccount, @@ -1970,7 +2036,7 @@ export class MangoClient { ); } - async altSet( + public async altSet( group: Group, addressLookupTable: PublicKey, index: number, @@ -1994,7 +2060,7 @@ export class MangoClient { ); } - async altExtend( + public async altExtend( group: Group, addressLookupTable: PublicKey, index: number, diff --git a/ts/client/src/scripts/devnet-admin.ts b/ts/client/src/scripts/devnet-admin.ts index a1f977514..c87984173 100644 --- a/ts/client/src/scripts/devnet-admin.ts +++ b/ts/client/src/scripts/devnet-admin.ts @@ -493,7 +493,7 @@ async function main() { try { let sig = await client.perpEditMarket( group, - 'BTC-PERP', + group.getPerpMarketByName('BTC-PERP').perpMarketIndex, btcDevnetOracle, 0.1, 6, diff --git a/ts/client/src/scripts/devnet-user.ts b/ts/client/src/scripts/devnet-user.ts index c379b5651..7d2ab01a8 100644 --- a/ts/client/src/scripts/devnet-user.ts +++ b/ts/client/src/scripts/devnet-user.ts @@ -1,5 +1,6 @@ -import { AnchorProvider, Wallet } from '@project-serum/anchor'; +import { AnchorProvider, BN, Wallet } from '@project-serum/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { expect } from 'chai'; import fs from 'fs'; import { HealthType } from '../accounts/mangoAccount'; import { BookSide, PerpOrderSide, PerpOrderType } from '../accounts/perp'; @@ -69,101 +70,132 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); let mangoAccount = (await client.getOrCreateMangoAccount(group))!; + await mangoAccount.reload(client); if (!mangoAccount) { throw new Error(`MangoAccount not found for user ${user.publicKey}`); } console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); - console.log(mangoAccount.toString(group)); - - await mangoAccount.reload(client); // set delegate, and change name if (true) { console.log(`...changing mango account name, and setting a delegate`); + const newName = 'my_changed_name'; const randomKey = new PublicKey( '4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo', ); - await client.editMangoAccount( - group, - mangoAccount, - 'my_changed_name', - randomKey, - ); + await client.editMangoAccount(group, mangoAccount, newName, randomKey); await mangoAccount.reload(client); - console.log(mangoAccount.toString()); + expect(mangoAccount.name).deep.equals(newName); + expect(mangoAccount.delegate).deep.equals(randomKey); + const oldName = 'my_mango_account'; console.log(`...resetting mango account name, and re-setting a delegate`); await client.editMangoAccount( group, mangoAccount, - 'my_mango_account', + oldName, PublicKey.default, ); await mangoAccount.reload(client); - console.log(mangoAccount.toString()); + expect(mangoAccount.name).deep.equals(oldName); + expect(mangoAccount.delegate).deep.equals(PublicKey.default); } // expand account - if (false) { + if ( + mangoAccount.tokens.length < 16 || + mangoAccount.serum3.length < 8 || + mangoAccount.perps.length < 8 || + mangoAccount.perpOpenOrders.length < 64 + ) { console.log( - `...expanding mango account to have serum3 and perp position slots`, + `...expanding mango account to max 16 token positions, 8 serum3, 8 perp position and 64 perp oo slots, previous (tokens ${mangoAccount.tokens.length}, serum3 ${mangoAccount.serum3.length}, perps ${mangoAccount.perps.length}, perps oo ${mangoAccount.perpOpenOrders.length})`, ); - await client.expandMangoAccount(group, mangoAccount, 8, 8, 8, 8); + let sig = await client.expandMangoAccount( + group, + mangoAccount, + 16, + 8, + 8, + 64, + ); + console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); await mangoAccount.reload(client); + expect(mangoAccount.tokens.length).equals(16); + expect(mangoAccount.serum3.length).equals(8); + expect(mangoAccount.perps.length).equals(8); + expect(mangoAccount.perpOpenOrders.length).equals(64); } // deposit and withdraw if (true) { - try { - console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`); - await client.tokenDeposit( - group, - mangoAccount, - new PublicKey(DEVNET_MINTS.get('USDC')!), - 50, - ); - await mangoAccount.reload(client); + console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`); - await client.tokenDeposit( - group, - mangoAccount, - new PublicKey(DEVNET_MINTS.get('SOL')!), - 1, - ); - await mangoAccount.reload(client); + // deposit USDC + let oldBalance = mangoAccount.getTokenBalance( + group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)), + ); + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('USDC')!), + 50, + ); + await mangoAccount.reload(client); + let newBalance = mangoAccount.getTokenBalance( + group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)), + ); + expect(toUiDecimalsForQuote(newBalance.sub(oldBalance)).toString()).equals( + '50', + ); - await client.tokenDeposit( - group, - mangoAccount, - new PublicKey(DEVNET_MINTS.get('MNGO')!), - 1, - ); - await mangoAccount.reload(client); + // deposit SOL + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('SOL')!), + 1, + ); + await mangoAccount.reload(client); - console.log(`...withdrawing 1 USDC`); - await client.tokenWithdraw( - group, - mangoAccount, - new PublicKey(DEVNET_MINTS.get('USDC')!), - 1, - true, - ); - await mangoAccount.reload(client); + // deposit MNGO + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('MNGO')!), + 1, + ); + await mangoAccount.reload(client); - console.log(`...depositing 0.0005 BTC`); - await client.tokenDeposit( - group, - mangoAccount, - new PublicKey(DEVNET_MINTS.get('BTC')!), - 0.0005, - ); - await mangoAccount.reload(client); + // withdraw USDC + console.log(`...withdrawing 1 USDC`); + oldBalance = mangoAccount.getTokenBalance( + group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)), + ); + await client.tokenWithdraw( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('USDC')!), + 1, + true, + ); + await mangoAccount.reload(client); + newBalance = mangoAccount.getTokenBalance( + group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)), + ); + expect(toUiDecimalsForQuote(oldBalance.sub(newBalance)).toString()).equals( + '1', + ); - console.log(mangoAccount.toString(group)); - } catch (error) { - console.log(error); - } + console.log(`...depositing 0.0005 BTC`); + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('BTC')!), + 0.0005, + ); + await mangoAccount.reload(client); } if (true) { @@ -177,7 +209,19 @@ async function main() { client, DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, ); - const highestBid = Array.from(asks!)![0]; + const highestBid = Array.from(bids!)![0]; + + console.log(`...cancelling all existing serum3 orders`); + if ( + Array.from(mangoAccount.serum3OosMapByMarketIndex.values()).length > 0 + ) { + await client.serum3CancelAllOrders( + group, + mangoAccount, + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + 10, + ); + } let price = 20; let qty = 0.0001; @@ -197,6 +241,13 @@ async function main() { 10, ); await mangoAccount.reload(client); + let orders = await mangoAccount.loadSerum3OpenOrdersForMarket( + client, + group, + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + ); + expect(orders[0].price).equals(20); + expect(orders[0].size).equals(qty); price = lowestAsk.price + lowestAsk.price / 2; qty = 0.0001; @@ -236,7 +287,7 @@ async function main() { ); console.log(`...current own orders on OB`); - let orders = await mangoAccount.loadSerum3OpenOrdersForMarket( + orders = await mangoAccount.loadSerum3OpenOrdersForMarket( client, group, DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, @@ -273,14 +324,6 @@ async function main() { ); } - if (true) { - // serum3 market - const serum3Market = group.serum3MarketsMapByExternal.get( - DEVNET_SERUM3_MARKETS.get('BTC/USDC')!.toBase58(), - ); - console.log(await serum3Market?.logOb(client, group)); - } - if (true) { await mangoAccount.reload(client); console.log( @@ -319,19 +362,7 @@ async function main() { } if (true) { - const asks = await group.loadSerum3AsksForMarket( - client, - DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, - ); - const lowestAsk = Array.from(asks!)[0]; - const bids = await group.loadSerum3BidsForMarket( - client, - DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, - ); - const highestBid = Array.from(asks!)![0]; - function getMaxSourceForTokenSwapWrapper(src, tgt) { - // console.log(); console.log( `getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` + mangoAccount.getMaxSourceUiForTokenSwap( @@ -409,15 +440,23 @@ async function main() { // bid max perp try { const clientId = Math.floor(Math.random() * 99999); + await mangoAccount.reload(client); + await group.reloadAll(client); const price = group.banksMapByName.get('BTC')![0].uiPrice! - Math.floor(Math.random() * 100); const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi( group, perpMarket.perpMarketIndex, - 1, ); const baseQty = quoteQty / price; + console.log( + ` simHealthRatioWithPerpBidUiChanges - ${mangoAccount.simHealthRatioWithPerpBidUiChanges( + group, + perpMarket.perpMarketIndex, + baseQty, + )}`, + ); console.log( `...placing max qty perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, ); @@ -457,8 +496,8 @@ async function main() { mangoAccount.getMaxQuoteForPerpBidUi( group, perpMarket.perpMarketIndex, - 1, ) * 1.02; + const baseQty = quoteQty / price; console.log( `...placing max qty * 1.02 perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, @@ -478,6 +517,7 @@ async function main() { ); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); } catch (error) { + console.log(error); console.log('Errored out as expected'); } @@ -490,7 +530,13 @@ async function main() { const baseQty = mangoAccount.getMaxBaseForPerpAskUi( group, perpMarket.perpMarketIndex, - 1, + ); + console.log( + ` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges( + group, + perpMarket.perpMarketIndex, + baseQty, + )}`, ); const quoteQty = baseQty * price; console.log( @@ -521,11 +567,8 @@ async function main() { group.banksMapByName.get('BTC')![0].uiPrice! + Math.floor(Math.random() * 100); const baseQty = - mangoAccount.getMaxBaseForPerpAskUi( - group, - perpMarket.perpMarketIndex, - 1, - ) * 1.02; + mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex) * + 1.02; const quoteQty = baseQty * price; console.log( `...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, @@ -545,6 +588,7 @@ async function main() { ); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); } catch (error) { + console.log(error); console.log('Errored out as expected'); } @@ -557,54 +601,54 @@ async function main() { ); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - // // scenario 2 - // // make + take orders - // try { - // const clientId = Math.floor(Math.random() * 99999); - // const price = group.banksMapByName.get('BTC')![0].uiPrice!; - // console.log(`...placing perp bid ${clientId} at ${price}`); - // const sig = await client.perpPlaceOrder( - // group, - // mangoAccount, - // perpMarket.perpMarketIndex, - // PerpOrderSide.bid, - // price, - // 0.01, - // price * 0.01, - // clientId, - // PerpOrderType.limit, - // 0, //Date.now() + 200, - // 1, - // ); - // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - // } catch (error) { - // console.log(error); - // } - // try { - // const clientId = Math.floor(Math.random() * 99999); - // const price = group.banksMapByName.get('BTC')![0].uiPrice!; - // console.log(`...placing perp ask ${clientId} at ${price}`); - // const sig = await client.perpPlaceOrder( - // group, - // mangoAccount, - // perpMarket.perpMarketIndex, - // PerpOrderSide.ask, - // price, - // 0.01, - // price * 0.011, - // clientId, - // PerpOrderType.limit, - // 0, //Date.now() + 200, - // 1, - // ); - // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - // } catch (error) { - // console.log(error); - // } - // // // should be able to cancel them : know bug - // // console.log(`...cancelling all perp orders`); - // // sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10); - // // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // scenario 2 + // make + take orders + try { + const clientId = Math.floor(Math.random() * 99999); + const price = group.banksMapByName.get('BTC')![0].uiPrice!; + console.log(`...placing perp bid ${clientId} at ${price}`); + const sig = await client.perpPlaceOrder( + group, + mangoAccount, + perpMarket.perpMarketIndex, + PerpOrderSide.bid, + price, + 0.01, + price * 0.01, + clientId, + PerpOrderType.limit, + 0, //Date.now() + 200, + 1, + ); + console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + } catch (error) { + console.log(error); + } + try { + const clientId = Math.floor(Math.random() * 99999); + const price = group.banksMapByName.get('BTC')![0].uiPrice!; + console.log(`...placing perp ask ${clientId} at ${price}`); + const sig = await client.perpPlaceOrder( + group, + mangoAccount, + perpMarket.perpMarketIndex, + PerpOrderSide.ask, + price, + 0.01, + price * 0.011, + clientId, + PerpOrderType.limit, + 0, //Date.now() + 200, + 1, + ); + console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + } catch (error) { + console.log(error); + } + // // should be able to cancel them : know bug + // console.log(`...cancelling all perp orders`); + // sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); const bids: BookSide = await perpMarket?.loadBids(client)!; console.log(`bids - ${Array.from(bids.items())}`); @@ -619,12 +663,14 @@ async function main() { console.log(`current funding rate per hour is ${fr}`); const eq = await perpMarket?.loadEventQueue(client)!; - console.log(`raw events - ${eq.rawEvents}`); + console.log( + `raw events - ${JSON.stringify(eq.eventsSince(new BN(0)), null, 2)}`, + ); // sleep so that keeper can catch up await new Promise((r) => setTimeout(r, 2000)); - // make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position + // make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position or we see a small quotePositionNative await group.reloadAll(client); await mangoAccount.reload(client); console.log(`${mangoAccount.toString(group)}`); diff --git a/ts/client/src/scripts/mm/log-perp-ob.ts b/ts/client/src/scripts/mm/log-perp-ob.ts new file mode 100644 index 000000000..dac67a2c3 --- /dev/null +++ b/ts/client/src/scripts/mm/log-perp-ob.ts @@ -0,0 +1,48 @@ +import { AnchorProvider, Wallet } from '@project-serum/anchor'; +import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { MangoClient } from '../../client'; +import { MANGO_V4_ID } from '../../constants'; + +// For easy switching between mainnet and devnet, default is mainnet +const CLUSTER: Cluster = + (process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta'; +const CLUSTER_URL = + process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL; +const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || ''; + +async function main() { + const options = AnchorProvider.defaultOptions(); + const connection = new Connection(CLUSTER_URL!, options); + + // Throwaway keypair + const user = new Keypair(); + const userWallet = new Wallet(user); + const userProvider = new AnchorProvider(connection, userWallet, options); + const client = await MangoClient.connect( + userProvider, + CLUSTER, + MANGO_V4_ID[CLUSTER], + {}, + 'get-program-accounts', + ); + + // Load mango account + let mangoAccount = await client.getMangoAccountForPublicKey( + new PublicKey(MANGO_ACCOUNT_PK), + ); + await mangoAccount.reload(client); + + // Load group for mango account + const group = await client.getGroup(mangoAccount.group); + await group.reloadAll(client); + + // Log OB + const perpMarket = group.getPerpMarketByName('BTC-PERP'); + while (true) { + await new Promise((r) => setTimeout(r, 2000)); + console.clear(); + console.log(await perpMarket.logOb(client)); + } +} + +main(); diff --git a/ts/client/src/scripts/mm/market-maker.ts b/ts/client/src/scripts/mm/market-maker.ts new file mode 100644 index 000000000..bb3788b6d --- /dev/null +++ b/ts/client/src/scripts/mm/market-maker.ts @@ -0,0 +1,514 @@ +import { AnchorProvider, BN, Wallet } from '@project-serum/anchor'; +import { + Cluster, + Connection, + Keypair, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import fs from 'fs'; +import path from 'path'; +import { Group } from '../../accounts/group'; +import { MangoAccount } from '../../accounts/mangoAccount'; +import { + BookSide, + PerpMarket, + PerpMarketIndex, + PerpOrderSide, + PerpOrderType, +} from '../../accounts/perp'; +import { MangoClient } from '../../client'; +import { MANGO_V4_ID } from '../../constants'; +import { toUiDecimalsForQuote } from '../../utils'; +import { sendTransaction } from '../../utils/rpc'; +import { + makeCheckAndSetSequenceNumberIx, + makeInitSequenceEnforcerAccountIx, + seqEnforcerProgramIds, +} from './sequence-enforcer-util'; + +// TODO switch to more efficient async logging if available in nodejs + +// Env vars +const CLUSTER: Cluster = + (process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta'; +const CLUSTER_URL = + process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL; +const USER_KEYPAIR = + process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR; +const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || ''; + +// Load configuration +const paramsFileName = process.env.PARAMS || 'default.json'; +const params = JSON.parse( + fs.readFileSync( + path.resolve(__dirname, `./params/${paramsFileName}`), + 'utf-8', + ), +); + +const control = { isRunning: true, interval: params.interval }; + +// State which is passed around +type State = { + mangoAccount: MangoAccount; + lastMangoAccountUpdate: number; + marketContexts: Map; +}; +type MarketContext = { + params: any; + market: PerpMarket; + bids: BookSide; + asks: BookSide; + lastBookUpdate: number; + + aggBid: number | undefined; + aggAsk: number | undefined; + ftxMid: number | undefined; + + sequenceAccount: PublicKey; + sequenceAccountBump: number; + + sentBidPrice: number; + sentAskPrice: number; + lastOrderUpdate: number; +}; + +// Refresh mango account and perp market order books +async function refreshState( + client: MangoClient, + group: Group, + mangoAccount: MangoAccount, + marketContexts: Map, +): Promise { + const ts = Date.now() / 1000; + + // TODO do all updates in one RPC call + await Promise.all([group.reloadAll(client), mangoAccount.reload(client)]); + + for (const perpMarket of Array.from( + group.perpMarketsMapByMarketIndex.values(), + )) { + const mc = marketContexts.get(perpMarket.perpMarketIndex)!; + mc.market = perpMarket; + mc.bids = await perpMarket.loadBids(client); + mc.asks = await perpMarket.loadAsks(client); + mc.lastBookUpdate = ts; + } + + return { + mangoAccount, + lastMangoAccountUpdate: ts, + marketContexts, + }; +} + +// Initialiaze sequence enforcer accounts +async function initSequenceEnforcerAccounts( + client: MangoClient, + marketContexts: MarketContext[], +) { + const seqAccIxs = marketContexts.map((mc) => + makeInitSequenceEnforcerAccountIx( + mc.sequenceAccount, + (client.program.provider as AnchorProvider).wallet.publicKey, + mc.sequenceAccountBump, + mc.market.name, + CLUSTER, + ), + ); + while (true) { + try { + const sig = await sendTransaction( + client.program.provider as AnchorProvider, + seqAccIxs, + [], + ); + console.log( + `Sequence enforcer accounts created, sig https://explorer.solana.com/tx/${sig}?cluster=${ + CLUSTER == 'devnet' ? 'devnet' : '' + }`, + ); + } catch (e) { + console.log('Failed to initialize sequence enforcer accounts!'); + console.log(e); + continue; + } + break; + } +} + +// Cancel all orders on exit +async function onExit( + client: MangoClient, + group: Group, + mangoAccount: MangoAccount, + marketContexts: MarketContext[], +) { + const ixs: TransactionInstruction[] = []; + for (const mc of marketContexts) { + const cancelAllIx = await client.perpCancelAllOrdersIx( + group, + mangoAccount, + mc.market.perpMarketIndex, + 10, + ); + ixs.push(cancelAllIx); + } + await sendTransaction(client.program.provider as AnchorProvider, ixs, []); +} + +// Main driver for the market maker +async function fullMarketMaker() { + // Load client + const options = AnchorProvider.defaultOptions(); + const connection = new Connection(CLUSTER_URL!, options); + const user = Keypair.fromSecretKey( + Buffer.from(JSON.parse(fs.readFileSync(USER_KEYPAIR!, 'utf-8'))), + ); + // TODO: make work for delegate + const userWallet = new Wallet(user); + const userProvider = new AnchorProvider(connection, userWallet, options); + const client = await MangoClient.connect( + userProvider, + CLUSTER, + MANGO_V4_ID[CLUSTER], + {}, + 'get-program-accounts', + ); + + // Load mango account + let mangoAccount = await client.getMangoAccountForPublicKey( + new PublicKey(MANGO_ACCOUNT_PK), + ); + console.log( + `MangoAccount ${mangoAccount.publicKey} for user ${user.publicKey}`, + ); + await mangoAccount.reload(client); + + // Load group + const group = await client.getGroup(mangoAccount.group); + await group.reloadAll(client); + + // Cancel all existing orders + for (const perpMarket of Array.from( + group.perpMarketsMapByMarketIndex.values(), + )) { + await client.perpCancelAllOrders( + group, + mangoAccount, + perpMarket.perpMarketIndex, + 10, + ); + } + + // Build and maintain an aggregate context object per market + const marketContexts: Map = new Map(); + for (const perpMarket of Array.from( + group.perpMarketsMapByMarketIndex.values(), + )) { + const [sequenceAccount, sequenceAccountBump] = + await PublicKey.findProgramAddress( + [ + Buffer.from(perpMarket.name, 'utf-8'), + ( + client.program.provider as AnchorProvider + ).wallet.publicKey.toBytes(), + ], + seqEnforcerProgramIds[CLUSTER], + ); + marketContexts.set(perpMarket.perpMarketIndex, { + params: params.assets[perpMarket.name.replace('-PERP', '')].perp, + market: perpMarket, + bids: await perpMarket.loadBids(client), + asks: await perpMarket.loadAsks(client), + lastBookUpdate: 0, + + sequenceAccount, + sequenceAccountBump, + + sentBidPrice: 0, + sentAskPrice: 0, + lastOrderUpdate: 0, + + // TODO + aggBid: undefined, + aggAsk: undefined, + ftxMid: undefined, + }); + } + + // Init sequence enforcer accounts + await initSequenceEnforcerAccounts( + client, + Array.from(marketContexts.values()), + ); + + // Load state first time + let state = await refreshState(client, group, mangoAccount, marketContexts); + + // Add handler for e.g. CTRL+C + process.on('SIGINT', function () { + console.log('Caught keyboard interrupt. Canceling orders'); + control.isRunning = false; + onExit(client, group, mangoAccount, Array.from(marketContexts.values())); + }); + + // Loop indefinitely + while (control.isRunning) { + try { + // TODO update this in a non blocking manner + state = await refreshState(client, group, mangoAccount, marketContexts); + + mangoAccount = state.mangoAccount; + + // Calculate pf level values + let pfQuoteValue: number | undefined = 0; + for (const mc of Array.from(marketContexts.values())) { + const pos = mangoAccount.getPerpPositionUi( + group, + mc.market.perpMarketIndex, + ); + // TODO use ftx to get mid then also combine with books from other exchanges + const midWorkaround = mc.market.uiPrice; + if (midWorkaround) { + pfQuoteValue += pos * midWorkaround; + } else { + pfQuoteValue = undefined; + console.log( + `Breaking pfQuoteValue computation, since mid is undefined for ${mc.market.name}!`, + ); + break; + } + } + + // Don't proceed if we don't have pfQuoteValue yet + if (pfQuoteValue === undefined) { + console.log( + `Continuing control loop, since pfQuoteValue is undefined!`, + ); + continue; + } + + // Update all orders on all markets + for (const mc of Array.from(marketContexts.values())) { + const ixs = await makeMarketUpdateInstructions( + client, + group, + mangoAccount, + mc, + pfQuoteValue, + ); + if (ixs.length === 0) { + continue; + } + + // TODO: batch ixs + const sig = await sendTransaction( + client.program.provider as AnchorProvider, + ixs, + group.addressLookupTablesList, + ); + console.log( + `Orders for market updated, sig https://explorer.solana.com/tx/${sig}?cluster=${ + CLUSTER == 'devnet' ? 'devnet' : '' + }`, + ); + } + } catch (e) { + console.log(e); + } finally { + console.log( + `${new Date().toUTCString()} sleeping for ${control.interval / 1000}s`, + ); + await new Promise((r) => setTimeout(r, control.interval)); + } + } +} + +async function makeMarketUpdateInstructions( + client: MangoClient, + group: Group, + mangoAccount: MangoAccount, + mc: MarketContext, + pfQuoteValue: number, +): Promise { + const perpMarketIndex = mc.market.perpMarketIndex; + const perpMarket = mc.market; + + const aggBid = perpMarket.uiPrice; // TODO mc.aggBid; + const aggAsk = perpMarket.uiPrice; // TODO mc.aggAsk; + if (aggBid === undefined || aggAsk === undefined) { + console.log(`No Aggregate Book for ${mc.market.name}!`); + return []; + } + + const leanCoeff = mc.params.leanCoeff; + + const fairValue = (aggBid + aggAsk) / 2; + const aggSpread = (aggAsk - aggBid) / fairValue; + + + const requoteThresh = mc.params.requoteThresh; + const equity = toUiDecimalsForQuote(mangoAccount.getEquity(group)); + const sizePerc = mc.params.sizePerc; + const quoteSize = equity * sizePerc; + const size = quoteSize / fairValue; + // TODO look at event queue as well for unprocessed fills + const basePos = mangoAccount.getPerpPositionUi(group, perpMarketIndex); + const lean = (-leanCoeff * basePos) / size; + const pfQuoteLeanCoeff = params.pfQuoteLeanCoeff || 0.001; // How much to move if pf pos is equal to equity + const pfQuoteLean = (pfQuoteValue / equity) * -pfQuoteLeanCoeff; + const charge = (mc.params.charge || 0.0015) + aggSpread / 2; + const bias = mc.params.bias; + const bidPrice = fairValue * (1 - charge + lean + bias + pfQuoteLean); + const askPrice = fairValue * (1 + charge + lean + bias + pfQuoteLean); + + // TODO volatility adjustment + + const modelBidPrice = perpMarket.uiPriceToLots(bidPrice); + const nativeBidSize = perpMarket.uiBaseToLots(size); + const modelAskPrice = perpMarket.uiPriceToLots(askPrice); + const nativeAskSize = perpMarket.uiBaseToLots(size); + + const bids = mc.bids; + const asks = mc.asks; + const bestBid = bids.best(); + const bestAsk = asks.best(); + const bookAdjBid = + bestAsk !== undefined + ? BN.min(bestAsk.priceLots.sub(new BN(1)), modelBidPrice) + : modelBidPrice; + const bookAdjAsk = + bestBid !== undefined + ? BN.max(bestBid.priceLots.add(new BN(1)), modelAskPrice) + : modelAskPrice; + + // TODO use order book to requote if size has changed + + // TODO + // const takeSpammers = mc.params.takeSpammers; + // const spammerCharge = mc.params.spammerCharge; + + let moveOrders = false; + if (mc.lastBookUpdate >= mc.lastOrderUpdate + 2) { + // console.log(` - moveOrders - 303`); + // If mango book was updated recently, then MangoAccount was also updated + const openOrders = await mangoAccount.loadPerpOpenOrdersForMarket( + client, + group, + perpMarketIndex, + ); + moveOrders = openOrders.length < 2 || openOrders.length > 2; + for (const o of openOrders) { + const refPrice = o.side === 'buy' ? bookAdjBid : bookAdjAsk; + moveOrders = + moveOrders || + Math.abs(o.priceLots.toNumber() / refPrice.toNumber() - 1) > + requoteThresh; + } + } else { + // console.log( + // ` - moveOrders - 319, mc.lastBookUpdate ${mc.lastBookUpdate}, mc.lastOrderUpdate ${mc.lastOrderUpdate}`, + // ); + // If order was updated before MangoAccount, then assume that sent order already executed + moveOrders = + moveOrders || + Math.abs(mc.sentBidPrice / bookAdjBid.toNumber() - 1) > requoteThresh || + Math.abs(mc.sentAskPrice / bookAdjAsk.toNumber() - 1) > requoteThresh; + } + + // Start building the transaction + const instructions: TransactionInstruction[] = [ + makeCheckAndSetSequenceNumberIx( + mc.sequenceAccount, + (client.program.provider as AnchorProvider).wallet.publicKey, + Date.now(), + CLUSTER, + ), + ]; + + // TODO Clear 1 lot size orders at the top of book that bad people use to manipulate the price + + if (moveOrders) { + // Cancel all, requote + const cancelAllIx = await client.perpCancelAllOrdersIx( + group, + mangoAccount, + perpMarketIndex, + 10, + ); + + const expiryTimestamp = + params.tif !== undefined ? Date.now() / 1000 + params.tif : 0; + + const placeBidIx = await client.perpPlaceOrderIx( + group, + mangoAccount, + perpMarketIndex, + PerpOrderSide.bid, + // TODO fix this, native to ui to native + perpMarket.priceLotsToUi(bookAdjBid), + perpMarket.baseLotsToUi(nativeBidSize), + undefined, + Date.now(), + PerpOrderType.postOnlySlide, + expiryTimestamp, + 20, + ); + + const placeAskIx = await client.perpPlaceOrderIx( + group, + mangoAccount, + perpMarketIndex, + PerpOrderSide.ask, + perpMarket.priceLotsToUi(bookAdjAsk), + perpMarket.baseLotsToUi(nativeAskSize), + undefined, + Date.now(), + PerpOrderType.postOnlySlide, + expiryTimestamp, + 20, + ); + + instructions.push(cancelAllIx); + const posAsTradeSizes = basePos / size; + if (posAsTradeSizes < 15) { + instructions.push(placeBidIx); + } + if (posAsTradeSizes > -15) { + instructions.push(placeAskIx); + } + console.log( + `Requoting for market ${mc.market.name} sentBid: ${ + mc.sentBidPrice + } newBid: ${bookAdjBid} sentAsk: ${ + mc.sentAskPrice + } newAsk: ${bookAdjAsk} pfLean: ${(pfQuoteLean * 10000).toFixed( + 1, + )} aggBid: ${aggBid} addAsk: ${aggAsk}`, + ); + mc.sentBidPrice = bookAdjBid.toNumber(); + mc.sentAskPrice = bookAdjAsk.toNumber(); + mc.lastOrderUpdate = Date.now() / 1000; + } else { + console.log( + `Not requoting for market ${mc.market.name}. No need to move orders`, + ); + } + + // If instruction is only the sequence enforcement, then just send empty + if (instructions.length === 1) { + return []; + } else { + return instructions; + } +} + +function startMarketMaker() { + if (control.isRunning) { + fullMarketMaker().finally(startMarketMaker); + } +} + +startMarketMaker(); diff --git a/ts/client/src/scripts/mm/params/default.json b/ts/client/src/scripts/mm/params/default.json new file mode 100644 index 000000000..132fe6a7d --- /dev/null +++ b/ts/client/src/scripts/mm/params/default.json @@ -0,0 +1,16 @@ +{ + "interval": 1000, + "batch": 1, + "assets": { + "BTC": { + "perp": { + "sizePerc": 0.05, + "leanCoeff": 0.00025, + "bias": 0.0, + "requoteThresh": 0.0002, + "takeSpammers": true, + "spammerCharge": 2 + } + } + } +} diff --git a/ts/client/src/scripts/mm/sequence-enforcer-util.ts b/ts/client/src/scripts/mm/sequence-enforcer-util.ts new file mode 100644 index 000000000..721aeb8eb --- /dev/null +++ b/ts/client/src/scripts/mm/sequence-enforcer-util.ts @@ -0,0 +1,68 @@ +import { BN } from '@project-serum/anchor'; +import { + PublicKey, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js'; +import { createHash } from 'crypto'; + +export const seqEnforcerProgramIds = { + devnet: new PublicKey('FBngRHN4s5cmHagqy3Zd6xcK3zPJBeX5DixtHFbBhyCn'), + testnet: new PublicKey('FThcgpaJM8WiEbK5rw3i31Ptb8Hm4rQ27TrhfzeR1uUy'), + 'mainnet-beta': new PublicKey('GDDMwNyyx8uB6zrqwBFHjLLG3TBYk2F8Az4yrQC5RzMp'), +}; + +export function makeInitSequenceEnforcerAccountIx( + account: PublicKey, + ownerPk: PublicKey, + bump: number, + sym: string, + cluster: string, +): TransactionInstruction { + const keys = [ + { isSigner: false, isWritable: true, pubkey: account }, + { isSigner: true, isWritable: true, pubkey: ownerPk }, + { isSigner: false, isWritable: false, pubkey: SystemProgram.programId }, + ]; + + const variant = createHash('sha256') + .update('global:initialize') + .digest() + .slice(0, 8); + + const bumpData = new BN(bump).toBuffer('le', 1); + const strLen = new BN(sym.length).toBuffer('le', 4); + const symEncoded = Buffer.from(sym); + + const data = Buffer.concat([variant, bumpData, strLen, symEncoded]); + + return new TransactionInstruction({ + keys, + data, + programId: seqEnforcerProgramIds[cluster], + }); +} + +export function makeCheckAndSetSequenceNumberIx( + sequenceAccount: PublicKey, + ownerPk: PublicKey, + seqNum: number, + cluster, +): TransactionInstruction { + const keys = [ + { isSigner: false, isWritable: true, pubkey: sequenceAccount }, + { isSigner: true, isWritable: false, pubkey: ownerPk }, + ]; + const variant = createHash('sha256') + .update('global:check_and_set_sequence_number') + .digest() + .slice(0, 8); + + const seqNumBuffer = new BN(seqNum).toBuffer('le', 8); + const data = Buffer.concat([variant, seqNumBuffer]); + return new TransactionInstruction({ + keys, + data, + programId: seqEnforcerProgramIds[cluster], + }); +} diff --git a/ts/client/src/scripts/mm/taker.ts b/ts/client/src/scripts/mm/taker.ts new file mode 100644 index 000000000..ef3f34a45 --- /dev/null +++ b/ts/client/src/scripts/mm/taker.ts @@ -0,0 +1,111 @@ +import { AnchorProvider, Wallet } from '@project-serum/anchor'; +import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js'; +import fs from 'fs'; +import { Group } from '../../accounts/group'; +import { MangoAccount } from '../../accounts/mangoAccount'; +import { PerpMarket, PerpOrderSide, PerpOrderType } from '../../accounts/perp'; +import { MangoClient } from '../../client'; +import { MANGO_V4_ID } from '../../constants'; +import { toUiDecimalsForQuote } from '../../utils'; + +// For easy switching between mainnet and devnet, default is mainnet +const CLUSTER: Cluster = + (process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta'; +const CLUSTER_URL = + process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL; +const USER_KEYPAIR = + process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR; +const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || ''; + +async function takeOrder( + client: MangoClient, + group: Group, + mangoAccount: MangoAccount, + perpMarket: PerpMarket, + side: PerpOrderSide, +) { + await mangoAccount.reload(client); + + const size = Math.random() * 0.001; + const price = + side === PerpOrderSide.bid + ? perpMarket.uiPrice * 1.01 + : perpMarket.uiPrice * 0.99; + console.log( + `${perpMarket.name} taking with a ${ + side === PerpOrderSide.bid ? 'bid' : 'ask' + } at price ${price.toFixed(4)} and size ${size.toFixed(6)}`, + ); + + const oldPosition = mangoAccount.getPerpPosition(perpMarket.perpMarketIndex); + if (oldPosition) { + console.log( + `- before base: ${perpMarket.baseLotsToUi( + oldPosition.basePositionLots, + )}, quote: ${toUiDecimalsForQuote(oldPosition.quotePositionNative)}`, + ); + } + + await client.perpPlaceOrder( + group, + mangoAccount, + perpMarket.perpMarketIndex, + side, + price, + size, + undefined, + Date.now(), + PerpOrderType.market, + 0, + 10, + ); + + // Sleep to see change, alternatively we could reload account with processed commitmment + await new Promise((r) => setTimeout(r, 5000)); + await mangoAccount.reload(client); + const newPosition = mangoAccount.getPerpPosition(perpMarket.perpMarketIndex); + if (newPosition) { + console.log( + `- after base: ${perpMarket.baseLotsToUi( + newPosition.basePositionLots, + )}, quote: ${toUiDecimalsForQuote(newPosition.quotePositionNative)}`, + ); + } +} + +async function main() { + const options = AnchorProvider.defaultOptions(); + const connection = new Connection(CLUSTER_URL!, options); + + const user = Keypair.fromSecretKey( + Buffer.from(JSON.parse(fs.readFileSync(USER_KEYPAIR!, 'utf-8'))), + ); + const userWallet = new Wallet(user); + const userProvider = new AnchorProvider(connection, userWallet, options); + const client = await MangoClient.connect( + userProvider, + CLUSTER, + MANGO_V4_ID[CLUSTER], + {}, + 'get-program-accounts', + ); + + // Load mango account + let mangoAccount = await client.getMangoAccountForPublicKey( + new PublicKey(MANGO_ACCOUNT_PK), + ); + await mangoAccount.reload(client); + + // Load group + const group = await client.getGroup(mangoAccount.group); + await group.reloadAll(client); + + // Take on OB + const perpMarket = group.getPerpMarketByName('BTC-PERP'); + while (true) { + await takeOrder(client, group, mangoAccount, perpMarket, PerpOrderSide.bid); + await takeOrder(client, group, mangoAccount, perpMarket, PerpOrderSide.ask); + } +} + +main(); diff --git a/ts/client/src/utils/rpc.ts b/ts/client/src/utils/rpc.ts index 8c01ea8a5..c60c7de20 100644 --- a/ts/client/src/utils/rpc.ts +++ b/ts/client/src/utils/rpc.ts @@ -51,6 +51,7 @@ export async function sendTransaction( latestBlockhash.blockhash != null && latestBlockhash.lastValidBlockHeight != null ) { + // TODO: tyler, can we remove these? console.log('confirming via blockhash'); status = ( await connection.confirmTransaction( @@ -63,6 +64,7 @@ export async function sendTransaction( ) ).value; } else { + // TODO: tyler, can we remove these? console.log('confirming via timeout'); status = (await connection.confirmTransaction(signature, 'processed')) .value; From 8f6fa5134ddfb61358a261658fbe6628eb2b5acd Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Fri, 7 Oct 2022 14:05:49 +0200 Subject: [PATCH 39/39] ts: comment out, tsc complains Signed-off-by: microwavedcola1 --- ts/client/src/utils/rpc.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/ts/client/src/utils/rpc.ts b/ts/client/src/utils/rpc.ts index c60c7de20..fc47a59db 100644 --- a/ts/client/src/utils/rpc.ts +++ b/ts/client/src/utils/rpc.ts @@ -4,7 +4,6 @@ import { Transaction, TransactionInstruction, } from '@solana/web3.js'; -import { buildVersionedTx } from '../utils'; export async function sendTransaction( provider: AnchorProvider, @@ -17,22 +16,17 @@ export async function sendTransaction( opts.preflightCommitment, ); - let tx: Transaction = new Transaction(); - const altsEnabled = false; - if (altsEnabled) { - tx = await buildVersionedTx(provider, ixs, opts.additionalSigners, alts); - } else { - const payer = (provider as AnchorProvider).wallet; - tx = new Transaction(); - tx.recentBlockhash = latestBlockhash.blockhash; - tx.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight; - tx.feePayer = payer.publicKey; - tx.add(...ixs); - if (opts.additionalSigners?.length > 0) { - tx.partialSign(...opts.additionalSigners); - } - await payer.signTransaction(tx); + const payer = (provider as AnchorProvider).wallet; + // const tx = await buildVersionedTx(provider, ixs, opts.additionalSigners, alts); + const tx = new Transaction(); + tx.recentBlockhash = latestBlockhash.blockhash; + tx.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight; + tx.feePayer = payer.publicKey; + tx.add(...ixs); + if (opts.additionalSigners?.length > 0) { + tx.partialSign(...opts.additionalSigners); } + await payer.signTransaction(tx); const signature = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true,