Merge deploy into dev

This commit is contained in:
Christian Kamm 2024-04-23 11:00:25 +02:00
commit 8fd7a5cc88
23 changed files with 1033 additions and 421 deletions

View File

@ -193,7 +193,7 @@ pub async fn loop_update_index_and_rate(
.map(|token_index| client.context.token(*token_index).name.to_owned())
.join(",");
let mut instructions = vec![];
let mut instructions = PreparedInstructions::new();
for token_index in token_indices_clone.iter() {
let token = client.context.token(*token_index);
let banks_for_a_token = token.banks();
@ -225,7 +225,14 @@ pub async fn loop_update_index_and_rate(
ix.accounts.append(&mut banks);
let sim_result = match client.simulate(vec![ix.clone()]).await {
let pix = PreparedInstructions::from_single(
ix,
client
.context
.compute_estimates
.cu_token_update_index_and_rates,
);
let sim_result = match client.simulate(pix.clone().to_instructions()).await {
Ok(response) => response.value,
Err(e) => {
error!(token.name, "simulation request error: {e:?}");
@ -238,28 +245,29 @@ pub async fn loop_update_index_and_rate(
continue;
}
instructions.push(ix);
instructions.append(pix);
}
let pre = Instant::now();
let sig_result = client
.send_and_confirm_permissionless_tx(instructions)
.send_and_confirm_permissionless_tx(instructions.to_instructions())
.await;
let confirmation_time = pre.elapsed().as_millis();
METRIC_CONFIRMATION_TIMES.observe(confirmation_time as f64);
let duration_ms = pre.elapsed().as_millis();
if let Err(e) = sig_result {
METRIC_UPDATE_TOKENS_FAILURE.inc();
info!(
"metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}",
token_names, confirmation_time, e
token_names, duration_ms, e
);
error!("{:?}", e)
} else {
METRIC_UPDATE_TOKENS_SUCCESS.inc();
METRIC_CONFIRMATION_TIMES.observe(duration_ms as f64);
info!(
"metricName=UpdateTokensV4Success tokens={} durationMs={}",
token_names, confirmation_time,
token_names, duration_ms,
);
info!("{:?}", sig_result);
}
@ -358,26 +366,37 @@ pub async fn loop_consume_events(
}),
};
let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).await;
let ixs = PreparedInstructions::from_single(
ix,
client.context.compute_estimates.cu_perp_consume_events_base
+ num_of_events
* client
.context
.compute_estimates
.cu_perp_consume_events_per_event,
);
let sig_result = client
.send_and_confirm_permissionless_tx(ixs.to_instructions())
.await;
let confirmation_time = pre.elapsed().as_millis();
METRIC_CONFIRMATION_TIMES.observe(confirmation_time as f64);
let duration_ms = pre.elapsed().as_millis();
if let Err(e) = sig_result {
METRIC_CONSUME_EVENTS_FAILURE.inc();
info!(
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
perp_market.name,
confirmation_time,
duration_ms,
num_of_events,
e.to_string()
);
error!("{:?}", e)
} else {
METRIC_CONSUME_EVENTS_SUCCESS.inc();
METRIC_CONFIRMATION_TIMES.observe(duration_ms as f64);
info!(
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
perp_market.name, confirmation_time, num_of_events,
perp_market.name, duration_ms, num_of_events,
);
info!("{:?}", sig_result);
}
@ -411,25 +430,31 @@ pub async fn loop_update_funding(
),
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpUpdateFunding {}),
};
let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).await;
let ixs = PreparedInstructions::from_single(
ix,
client.context.compute_estimates.cu_perp_update_funding,
);
let sig_result = client
.send_and_confirm_permissionless_tx(ixs.to_instructions())
.await;
let confirmation_time = pre.elapsed().as_millis();
METRIC_CONFIRMATION_TIMES.observe(confirmation_time as f64);
let duration_ms = pre.elapsed().as_millis();
if let Err(e) = sig_result {
METRIC_UPDATE_FUNDING_FAILURE.inc();
error!(
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
perp_market.name,
confirmation_time,
duration_ms,
e.to_string()
);
error!("{:?}", e)
} else {
METRIC_UPDATE_FUNDING_SUCCESS.inc();
METRIC_CONFIRMATION_TIMES.observe(duration_ms as f64);
info!(
"metricName=UpdateFundingV4Success market={} durationMs={}",
perp_market.name, confirmation_time,
perp_market.name, duration_ms,
);
info!("{:?}", sig_result);
}

View File

@ -324,7 +324,7 @@ impl<'a> LiquidateHelper<'a> {
let txsig = self
.client
.send_and_confirm_owner_tx(liq_ixs.to_instructions())
.send_and_confirm_authority_tx(liq_ixs.to_instructions())
.await
.context("sending perp_liq_base_or_positive_pnl_instruction")?;
info!(
@ -370,7 +370,7 @@ impl<'a> LiquidateHelper<'a> {
liq_ixs.cu = liq_ixs.cu.max(self.config.compute_limit_for_liq_ix);
let txsig = self
.client
.send_and_confirm_owner_tx(liq_ixs.to_instructions())
.send_and_confirm_authority_tx(liq_ixs.to_instructions())
.await
.context("sending perp_liq_negative_pnl_or_bankruptcy_instruction")?;
info!(
@ -578,7 +578,7 @@ impl<'a> LiquidateHelper<'a> {
let txsig = self
.client
.send_and_confirm_owner_tx(liq_ixs.to_instructions())
.send_and_confirm_authority_tx(liq_ixs.to_instructions())
.await
.context("sending liq_token_with_token")?;
info!(
@ -635,7 +635,7 @@ impl<'a> LiquidateHelper<'a> {
liq_ixs.cu = liq_ixs.cu.max(self.config.compute_limit_for_liq_ix);
let txsig = self
.client
.send_and_confirm_owner_tx(liq_ixs.to_instructions())
.send_and_confirm_authority_tx(liq_ixs.to_instructions())
.await
.context("sending liq_token_with_token")?;
info!(

View File

@ -126,9 +126,16 @@ async fn main() -> anyhow::Result<()> {
let mango_oracles = group_context
.tokens
.values()
.map(|value| value.oracle)
.flat_map(|value| {
[
value.oracle,
value.fallback_context.key,
value.fallback_context.quote_key,
]
})
.chain(group_context.perp_markets.values().map(|p| p.oracle))
.unique()
.filter(|&k| k != Pubkey::default())
.collect::<Vec<Pubkey>>();
let serum_programs = group_context

View File

@ -472,7 +472,7 @@ impl Rebalancer {
// Imagine SOL at 0.04 USDC-native per SOL-native: Any amounts below 25 SOL-native
// would not be worth a single USDC-native.
//
// To avoid errors, we consider all amounts below 2 * (1/oracle) dust and don't try
// To avoid errors, we consider all amounts below 1000 * (1/oracle) dust and don't try
// to sell them. Instead they will be withdrawn at the end.
// Purchases will aim to purchase slightly more than is needed, such that we can
// again withdraw the dust at the end.
@ -693,7 +693,7 @@ impl Rebalancer {
let txsig = self
.mango_client
.send_and_confirm_owner_tx(ixs.to_instructions())
.send_and_confirm_authority_tx(ixs.to_instructions())
.await?;
info!(
@ -765,7 +765,7 @@ impl Rebalancer {
let txsig = self
.mango_client
.send_and_confirm_owner_tx(ixs.to_instructions())
.send_and_confirm_authority_tx(ixs.to_instructions())
.await?;
info!(
@ -937,7 +937,7 @@ impl Rebalancer {
let tx_builder = TransactionBuilder {
instructions: ixs.to_instructions(),
signers: vec![self.mango_client.owner.clone()],
signers: vec![self.mango_client.authority.clone()],
..self.mango_client.transaction_builder().await?
};

View File

@ -21,9 +21,9 @@ async fn report(client: &MangoClient, min_health_ratio: f64) -> anyhow::Result<(
})
.to_string();
let signature = client.owner.sign_message(message.as_bytes());
let signature = client.authority.sign_message(message.as_bytes());
let payload = serde_json::json!({
"wallet_pk": client.owner.pubkey().to_string(),
"wallet_pk": client.authority.pubkey().to_string(),
"message": message,
"signature": signature.to_string(),
});

View File

@ -1211,7 +1211,7 @@ impl Context {
let fee_payer = self.mango_client.client.fee_payer();
TransactionBuilder {
instructions: vec![compute_ix],
signers: vec![self.mango_client.owner.clone(), fee_payer],
signers: vec![self.mango_client.authority.clone(), fee_payer],
..self.mango_client.transaction_builder().await?
}
};

View File

@ -142,7 +142,7 @@ impl State {
}
let txsig = match mango_client
.send_and_confirm_owner_tx(instructions.to_instructions())
.send_and_confirm_authority_tx(instructions.to_instructions())
.await
{
Ok(v) => v,

View File

@ -274,7 +274,7 @@ pub struct MangoClient {
// call to refresh banks etc -- if it's backed by websockets, these could just do nothing
pub account_fetcher: Arc<dyn AccountFetcher>,
pub owner: Arc<Keypair>,
pub authority: Arc<Keypair>,
pub mango_account_address: Pubkey,
pub context: MangoGroupContext,
@ -407,7 +407,7 @@ impl MangoClient {
pub async fn new_for_existing_account(
client: Client,
account: Pubkey,
owner: Arc<Keypair>,
authority: Arc<Keypair>,
) -> anyhow::Result<Self> {
let rpc = client.new_rpc_async();
let account_fetcher = Arc::new(CachedAccountFetcher::new(Arc::new(RpcAccountFetcher {
@ -416,25 +416,25 @@ impl MangoClient {
let mango_account =
account_fetcher_fetch_mango_account(&*account_fetcher, &account).await?;
let group = mango_account.fixed.group;
if mango_account.fixed.owner != owner.pubkey() {
if mango_account.fixed.owner != authority.pubkey() {
anyhow::bail!(
"bad owner for account: expected {} got {}",
mango_account.fixed.owner,
owner.pubkey()
authority.pubkey()
);
}
let rpc = client.rpc_async();
let group_context = MangoGroupContext::new_from_rpc(&rpc, group).await?;
Self::new_detail(client, account, owner, group_context, account_fetcher)
Self::new_detail(client, account, authority, group_context, account_fetcher)
}
/// Allows control of AccountFetcher and externally created MangoGroupContext
pub fn new_detail(
client: Client,
account: Pubkey,
owner: Arc<Keypair>,
authority: Arc<Keypair>,
// future: maybe pass Arc<MangoGroupContext>, so it can be extenally updated?
group_context: MangoGroupContext,
account_fetcher: Arc<dyn AccountFetcher>,
@ -442,15 +442,15 @@ impl MangoClient {
Ok(Self {
client,
account_fetcher,
owner,
authority,
mango_account_address: account,
context: group_context,
http_client: reqwest::Client::new(),
})
}
pub fn owner(&self) -> Pubkey {
self.owner.pubkey()
pub fn authority(&self) -> Pubkey {
self.authority.pubkey()
}
pub fn group(&self) -> Pubkey {
@ -555,12 +555,15 @@ impl MangoClient {
&mango_v4::accounts::TokenDeposit {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
owner: self.authority(),
bank: token.first_bank(),
vault: token.first_vault(),
oracle: token.oracle,
token_account: get_associated_token_address(&self.owner(), &token.mint),
token_authority: self.owner(),
token_account: get_associated_token_address(
&self.authority(),
&token.mint,
),
token_authority: self.authority(),
token_program: Token::id(),
},
None,
@ -575,7 +578,8 @@ impl MangoClient {
},
self.instruction_cu(health_cu),
);
self.send_and_confirm_owner_tx(ixs.to_instructions()).await
self.send_and_confirm_authority_tx(ixs.to_instructions())
.await
}
/// Assert that health of account is > N
@ -668,8 +672,8 @@ impl MangoClient {
let ixs = PreparedInstructions::from_vec(
vec![
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
&self.owner(),
&self.owner(),
&self.authority(),
&account.fixed.owner,
&mint,
&Token::id(),
),
@ -680,12 +684,12 @@ impl MangoClient {
&mango_v4::accounts::TokenWithdraw {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
owner: self.authority(),
bank: token.first_bank(),
vault: token.first_vault(),
oracle: token.oracle,
token_account: get_associated_token_address(
&self.owner(),
&account.fixed.owner,
&token.mint,
),
token_program: Token::id(),
@ -716,7 +720,8 @@ impl MangoClient {
let ixs = self
.token_withdraw_instructions(&account, mint, amount, allow_borrow)
.await?;
self.send_and_confirm_owner_tx(ixs.to_instructions()).await
self.send_and_confirm_authority_tx(ixs.to_instructions())
.await
}
pub async fn bank_oracle_price(&self, token_index: TokenIndex) -> anyhow::Result<I80F48> {
@ -768,8 +773,8 @@ impl MangoClient {
serum_program: s3.serum_program,
serum_market_external: s3.serum_market_external,
open_orders,
owner: self.owner(),
sol_destination: self.owner(),
owner: self.authority(),
sol_destination: self.authority(),
},
None,
),
@ -784,7 +789,8 @@ impl MangoClient {
pub async fn serum3_close_open_orders(&self, name: &str) -> anyhow::Result<Signature> {
let market_index = self.context.serum3_market_index(name);
let ix = self.serum3_close_open_orders_instruction(market_index);
self.send_and_confirm_owner_tx(ix.to_instructions()).await
self.send_and_confirm_authority_tx(ix.to_instructions())
.await
}
pub fn serum3_create_open_orders_instruction(
@ -806,8 +812,8 @@ impl MangoClient {
serum_program: s3.serum_program,
serum_market_external: s3.serum_market_external,
open_orders,
owner: self.owner(),
payer: self.owner(),
owner: self.authority(),
payer: self.authority(),
system_program: System::id(),
rent: sysvar::rent::id(),
},
@ -839,7 +845,7 @@ impl MangoClient {
pub async fn serum3_create_open_orders(&self, name: &str) -> anyhow::Result<Signature> {
let market_index = self.context.serum3_market_index(name);
let ix = self.serum3_create_open_orders_instruction(market_index);
self.send_and_confirm_owner_tx(vec![ix]).await
self.send_and_confirm_authority_tx(vec![ix]).await
}
#[allow(clippy::too_many_arguments)]
@ -897,7 +903,7 @@ impl MangoClient {
market_base_vault: s3.coin_vault,
market_quote_vault: s3.pc_vault,
market_vault_signer: s3.vault_signer,
owner: self.owner(),
owner: self.authority(),
token_program: Token::id(),
},
None,
@ -1160,7 +1166,8 @@ impl MangoClient {
let mut ixs = PreparedInstructions::new();
ixs.append(create_or_replace_ixs);
ixs.append(place_order_ixs);
self.send_and_confirm_owner_tx(ixs.to_instructions()).await
self.send_and_confirm_authority_tx(ixs.to_instructions())
.await
}
pub async fn serum3_settle_funds(&self, name: &str) -> anyhow::Result<Signature> {
@ -1173,7 +1180,8 @@ impl MangoClient {
let open_orders = account.serum3_orders(market_index).unwrap().open_orders;
let ix = self.serum3_settle_funds_instruction(s3, base, quote, open_orders);
self.send_and_confirm_owner_tx(ix.to_instructions()).await
self.send_and_confirm_authority_tx(ix.to_instructions())
.await
}
pub fn serum3_settle_funds_instruction(
@ -1201,7 +1209,7 @@ impl MangoClient {
market_base_vault: s3.coin_vault,
market_quote_vault: s3.pc_vault,
market_vault_signer: s3.vault_signer,
owner: self.owner(),
owner: self.authority(),
token_program: Token::id(),
},
v2: mango_v4::accounts::Serum3SettleFundsV2Extra {
@ -1245,7 +1253,7 @@ impl MangoClient {
serum_market: s3.address,
serum_program: s3.serum_program,
serum_market_external: s3.serum_market_external,
owner: self.owner(),
owner: self.authority(),
},
None,
),
@ -1366,7 +1374,7 @@ impl MangoClient {
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::OpenbookV2LiqForceCancelOrders {
payer: self.owner(),
payer: self.authority(),
group: self.group(),
account: *liqee.0,
open_orders: *open_orders,
@ -1440,7 +1448,7 @@ impl MangoClient {
market_bids: s3.bids,
market_asks: s3.asks,
market_event_queue: s3.event_q,
owner: self.owner(),
owner: self.authority(),
},
None,
)
@ -1450,7 +1458,7 @@ impl MangoClient {
order_id,
}),
};
self.send_and_confirm_owner_tx(vec![ix]).await
self.send_and_confirm_authority_tx(vec![ix]).await
}
//
@ -1503,7 +1511,7 @@ impl MangoClient {
&mango_v4::accounts::PerpPlaceOrder {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
owner: self.authority(),
perp_market: perp.address,
bids: perp.bids,
asks: perp.asks,
@ -1608,7 +1616,8 @@ impl MangoClient {
self_trade_behavior,
)
.await?;
self.send_and_confirm_owner_tx(ixs.to_instructions()).await
self.send_and_confirm_authority_tx(ixs.to_instructions())
.await
}
pub fn perp_cancel_all_orders_instruction(
@ -1626,7 +1635,7 @@ impl MangoClient {
&mango_v4::accounts::PerpCancelAllOrders {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
owner: self.authority(),
perp_market: perp.address,
bids: perp.bids,
asks: perp.asks,
@ -1651,7 +1660,8 @@ impl MangoClient {
let ixs = self
.perp_deactivate_position_instruction(market_index)
.await?;
self.send_and_confirm_owner_tx(ixs.to_instructions()).await
self.send_and_confirm_authority_tx(ixs.to_instructions())
.await
}
async fn perp_deactivate_position_instruction(
@ -1668,7 +1678,7 @@ impl MangoClient {
&mango_v4::accounts::PerpDeactivatePosition {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
owner: self.authority(),
perp_market: perp.address,
},
None,
@ -1711,7 +1721,7 @@ impl MangoClient {
&mango_v4::accounts::PerpSettlePnl {
group: self.group(),
settler: self.mango_account_address,
settler_owner: self.owner(),
settler_owner: self.authority(),
perp_market: perp.address,
account_a: *account_a.0,
account_b: *account_b.0,
@ -1815,7 +1825,7 @@ impl MangoClient {
perp_market: perp.address,
oracle: perp.oracle,
liqor: self.mango_account_address,
liqor_owner: self.owner(),
liqor_owner: self.authority(),
liqee: *liqee.0,
settle_bank: settle_token_info.first_bank(),
settle_vault: settle_token_info.first_vault(),
@ -1875,7 +1885,7 @@ impl MangoClient {
perp_market: perp.address,
oracle: perp.oracle,
liqor: self.mango_account_address,
liqor_owner: self.owner(),
liqor_owner: self.authority(),
liqee: *liqee.0,
settle_bank: settle_token_info.first_bank(),
settle_vault: settle_token_info.first_vault(),
@ -1983,7 +1993,7 @@ impl MangoClient {
group: self.group(),
liqee: *liqee.0,
liqor: self.mango_account_address,
liqor_owner: self.owner(),
liqor_owner: self.authority(),
},
None,
);
@ -2044,7 +2054,7 @@ impl MangoClient {
group: self.group(),
liqee: *liqee.0,
liqor: self.mango_account_address,
liqor_owner: self.owner(),
liqor_owner: self.authority(),
liab_mint_info: liab_info.mint_info_address,
quote_vault: quote_info.first_vault(),
insurance_vault: group.insurance_vault,
@ -2104,7 +2114,7 @@ impl MangoClient {
group: self.group(),
liqee: *liqee.0,
liqor: self.mango_account_address,
liqor_authority: self.owner(),
liqor_authority: self.authority(),
},
None,
);
@ -2151,7 +2161,7 @@ impl MangoClient {
group: self.group(),
liqee: *account.0,
liqor: self.mango_account_address,
liqor_authority: self.owner(),
liqor_authority: self.authority(),
},
None,
);
@ -2355,7 +2365,7 @@ impl MangoClient {
self.context.compute_estimates.cu_per_mango_instruction + health_cu
}
pub async fn send_and_confirm_owner_tx(
pub async fn send_and_confirm_authority_tx(
&self,
instructions: Vec<Instruction>,
) -> anyhow::Result<Signature> {
@ -2363,7 +2373,7 @@ impl MangoClient {
instructions,
..self.transaction_builder().await?
};
tx_builder.signers.push(self.owner.clone());
tx_builder.signers.push(self.authority.clone());
tx_builder.send_and_confirm(&self.client).await
}

View File

@ -145,6 +145,10 @@ pub struct ComputeEstimates {
pub cu_per_charge_collateral_fees_token: u32,
pub cu_for_sequence_check: u32,
pub cu_per_associated_token_account_creation: u32,
pub cu_perp_update_funding: u32,
pub cu_perp_consume_events_base: u32,
pub cu_perp_consume_events_per_event: u32,
pub cu_token_update_index_and_rates: u32,
}
impl Default for ComputeEstimates {
@ -173,6 +177,10 @@ impl Default for ComputeEstimates {
// measured around 8k, see test_basics
cu_for_sequence_check: 10_000,
cu_per_associated_token_account_creation: 21_000,
cu_perp_update_funding: 40_000,
cu_perp_consume_events_base: 10_000,
cu_perp_consume_events_per_event: 18_000,
cu_token_update_index_and_rates: 90_000,
}
}
}

View File

@ -233,14 +233,14 @@ impl<'a> JupiterV6<'a> {
.map(util::to_writable_account_meta)
.collect::<Vec<_>>();
let owner = self.mango_client.owner();
let authority = self.mango_client.authority();
let account = &self.mango_client.mango_account().await?;
let token_ams = [source_token.mint, target_token.mint]
.into_iter()
.map(|mint| {
util::to_writable_account_meta(
anchor_spl::associated_token::get_associated_token_address(&owner, &mint),
anchor_spl::associated_token::get_associated_token_address(&authority, &mint),
)
})
.collect::<Vec<_>>();
@ -277,7 +277,7 @@ impl<'a> JupiterV6<'a> {
.post(format!("{}/swap-instructions", config.jupiter_v6_url))
.query(&query_args)
.json(&SwapRequest {
user_public_key: owner.to_string(),
user_public_key: authority.to_string(),
wrap_and_unwrap_sol: false,
use_shared_accounts: true,
fee_account: None,
@ -308,8 +308,8 @@ impl<'a> JupiterV6<'a> {
// Ensure the source token account is created (jupiter takes care of the output account)
instructions.push(
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
&owner,
&owner,
&authority,
&authority,
&source_token.mint,
&Token::id(),
),
@ -321,7 +321,7 @@ impl<'a> JupiterV6<'a> {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::FlashLoanBegin {
account: self.mango_client.mango_account_address,
owner,
owner: authority,
token_program: Token::id(),
instructions: solana_sdk::sysvar::instructions::id(),
},
@ -344,7 +344,7 @@ impl<'a> JupiterV6<'a> {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::FlashLoanEnd {
account: self.mango_client.mango_account_address,
owner,
owner: authority,
token_program: Token::id(),
},
None,
@ -379,13 +379,13 @@ impl<'a> JupiterV6<'a> {
.await?;
address_lookup_tables.extend(jup_alts.into_iter());
let payer = owner; // maybe use fee_payer? but usually it's the same
let payer = authority; // maybe use fee_payer? but usually it's the same
Ok(TransactionBuilder {
instructions,
address_lookup_tables,
payer,
signers: vec![self.mango_client.owner.clone()],
signers: vec![self.mango_client.authority.clone()],
config: self
.mango_client
.client

View File

@ -124,14 +124,14 @@ impl<'a> Sanctum<'a> {
.map(util::to_writable_account_meta)
.collect::<Vec<_>>();
let owner = self.mango_client.owner();
let authority = self.mango_client.authority();
let account = &self.mango_client.mango_account().await?;
let token_ams = [source_token.mint, target_token.mint]
.into_iter()
.map(|mint| {
util::to_writable_account_meta(
anchor_spl::associated_token::get_associated_token_address(&owner, &mint),
anchor_spl::associated_token::get_associated_token_address(&authority, &mint),
)
})
.collect::<Vec<_>>();
@ -176,7 +176,7 @@ impl<'a> Sanctum<'a> {
input: input_mint.to_string(),
mode: "ExactIn".to_string(),
output_lst_mint: output_mint.to_string(),
signer: owner.to_string(),
signer: authority.to_string(),
swap_src: quote.swap_src.clone(),
})
.timeout(self.timeout_duration)
@ -244,8 +244,8 @@ impl<'a> Sanctum<'a> {
// Ensure the source token account is created (sanctum takes care of the output account)
instructions.push(
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
&owner,
&owner,
&authority,
&authority,
&source_token.mint,
&Token::id(),
),
@ -257,7 +257,7 @@ impl<'a> Sanctum<'a> {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::FlashLoanBegin {
account: self.mango_client.mango_account_address,
owner,
owner: authority,
token_program: Token::id(),
instructions: solana_sdk::sysvar::instructions::id(),
},
@ -284,7 +284,7 @@ impl<'a> Sanctum<'a> {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::FlashLoanEnd {
account: self.mango_client.mango_account_address,
owner,
owner: authority,
token_program: Token::id(),
},
None,
@ -308,13 +308,13 @@ impl<'a> Sanctum<'a> {
let mut address_lookup_tables = self.mango_client.mango_address_lookup_tables().await?;
address_lookup_tables.extend(sanctum_alts.into_iter());
let payer = owner; // maybe use fee_payer? but usually it's the same
let payer = authority; // maybe use fee_payer? but usually it's the same
Ok(TransactionBuilder {
instructions,
address_lookup_tables,
payer,
signers: vec![self.mango_client.owner.clone()],
signers: vec![self.mango_client.authority.clone()],
config: self
.mango_client
.client

View File

@ -1,6 +1,6 @@
{
"name": "@blockworks-foundation/mango-v4",
"version": "0.23.1",
"version": "0.24.0",
"description": "Typescript Client for mango-v4 program.",
"repository": "https://github.com/blockworks-foundation/mango-v4",
"author": {
@ -27,8 +27,8 @@
"lint": "eslint ./ts/client/src --ext ts --ext tsx --ext js --quiet",
"typecheck": "tsc --noEmit --pretty",
"prepublishOnly": "yarn validate && yarn build",
"validate": "yarn lint && yarn format",
"deduplicate": "npx yarn-deduplicate --list --fail",
"validate": "yarn lint && yarn format",
"prepare": "yarn build"
},
"devDependencies": {
@ -64,7 +64,7 @@
},
"dependencies": {
"@blockworks-foundation/mango-v4-settings": "0.14.15",
"@blockworks-foundation/mangolana": "0.0.14",
"@blockworks-foundation/mangolana": "0.0.15",
"@coral-xyz/anchor": "^0.29.0",
"@openbook-dex/openbook-v2": "^0.1.2",
"@project-serum/serum": "0.13.65",

View File

@ -0,0 +1,86 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Commitment, Connection, Keypair, PublicKey } from '@solana/web3.js';
import * as fs from 'fs';
import { MangoAccount, MangoClient, TokenIndex } from '../src';
const outputCsvName = 'mango-boost-switchboard-snapshot.csv';
const script = async () => {
const connection = new Connection(
process.env.MB_CLUSTER_URL!,
'confirmed' as Commitment,
);
const kp = Keypair.generate();
const wallet = new Wallet(kp);
const provider = new AnchorProvider(connection, wallet, {});
const CLUSTER = 'mainnet-beta';
const client = MangoClient.connect(
provider as any,
CLUSTER,
new PublicKey('zF2vSz6V9g1YHGmfrzsY497NJzbRr84QUrPry4bLQ25'),
{ idsSource: 'get-program-accounts' },
);
const groupKey = new PublicKey(
'AKeMSYiJekyKfwCc3CUfVNDVAiqk9FfbQVMY3G7RUZUf',
);
const group = await client.getGroup(groupKey);
const mangoAccounts = await client.getAllMangoAccounts(group, false);
const sbTokenIndexes = [
1, // JLP
];
const sbBanks = await Promise.all(
sbTokenIndexes.map((i) => group.getFirstBankByTokenIndex(i as TokenIndex)),
);
function accountHasSbTokens(m: MangoAccount): boolean {
for (const bank of sbBanks) {
if (m.getTokenBalanceUi(bank) !== 0) {
return true;
}
}
return false;
}
const sbAccounts = mangoAccounts.filter((m) => accountHasSbTokens(m));
const resultMap = new Map<string, [number, number]>();
for (const account of sbAccounts) {
let deposits = 0;
let borrows = 0;
for (const bank of sbBanks) {
deposits += account.getTokenDepositsUi(bank) * bank.uiPrice;
borrows += account.getTokenBorrowsUi(bank) * bank.uiPrice;
}
const wallet = account.owner.toBase58();
const existingValue = resultMap.get(account.owner.toBase58());
if (existingValue) {
const [d, b] = existingValue;
resultMap.set(wallet, [d + deposits, b + borrows]);
} else {
resultMap.set(wallet, [deposits, borrows]);
}
}
// console.log(resultMap.entries())
writeMapToCsv(resultMap, outputCsvName);
};
script();
function writeMapToCsv(
map: Map<string, [number, number]>,
filename: string,
): void {
let csv = 'wallet,deposits_value,borrows_value\n';
for (let [wallet, [deposits, borrows]] of map) {
csv += `${wallet},${deposits},${borrows}\n`;
}
fs.writeFileSync(filename, csv);
}

View File

@ -0,0 +1,95 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Commitment, Connection, Keypair, PublicKey } from '@solana/web3.js';
import * as fs from 'fs';
import { MANGO_V4_ID, MangoAccount, MangoClient, TokenIndex } from '../src';
const outputCsvName = 'mango-switchboard-snapshot.csv';
const script = async () => {
const connection = new Connection(
process.env.MB_CLUSTER_URL!,
'confirmed' as Commitment,
);
const kp = Keypair.generate();
const wallet = new Wallet(kp);
const provider = new AnchorProvider(connection, wallet, {});
const CLUSTER = 'mainnet-beta';
const client = MangoClient.connect(
provider as any,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{ idsSource: 'api' },
);
const groupKey = new PublicKey(
'78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX',
);
const group = await client.getGroup(groupKey);
const mangoAccounts = await client.getAllMangoAccounts(group, false);
const sbTokenIndexes = [
// https://api.mngo.cloud/data/v4/group-metadata
791, // NOS
916, // STEP
881, // GECKO
889, // Moutai
743, // JLP
669, // GUAC
455, // DUAL
616, // ALL
848, // WEN
];
const sbBanks = await Promise.all(
sbTokenIndexes.map((i) => group.getFirstBankByTokenIndex(i as TokenIndex)),
);
function accountHasSbTokens(m: MangoAccount): boolean {
for (const bank of sbBanks) {
if (m.getTokenBalanceUi(bank) !== 0) {
return true;
}
}
return false;
}
const sbAccounts = mangoAccounts.filter((m) => accountHasSbTokens(m));
const resultMap = new Map<string, [number, number]>();
for (const account of sbAccounts) {
let deposits = 0;
let borrows = 0;
for (const bank of sbBanks) {
deposits += account.getTokenDepositsUi(bank) * bank.uiPrice;
borrows += account.getTokenBorrowsUi(bank) * bank.uiPrice;
}
const wallet = account.owner.toBase58();
const existingValue = resultMap.get(account.owner.toBase58());
if (existingValue) {
const [d, b] = existingValue;
resultMap.set(wallet, [d + deposits, b + borrows]);
} else {
resultMap.set(wallet, [deposits, borrows]);
}
}
// console.log(resultMap.entries())
writeMapToCsv(resultMap, outputCsvName);
};
script();
function writeMapToCsv(
map: Map<string, [number, number]>,
filename: string,
): void {
let csv = 'wallet,deposits_value,borrows_value\n';
for (let [wallet, [deposits, borrows]] of map) {
csv += `${wallet},${deposits},${borrows}\n`;
}
fs.writeFileSync(filename, csv);
}

View File

@ -248,7 +248,10 @@ async function fullMarketMaker() {
CLUSTER,
MANGO_V4_ID[CLUSTER],
{
idsSource: 'get-program-accounts',
idsSource: 'api',
fallbackOracleConfig: [
new PublicKey('Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD'), // USDC pyth oracle
],
},
);

View File

@ -0,0 +1,70 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { TokenIndex } from '../src/accounts/bank';
import { MangoClient } from '../src/client';
import { MANGO_V4_ID } from '../src/constants';
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;
const TOKEN_INDEX = Number(process.env.TOKEN_INDEX) as TokenIndex;
async function tokenDeposit(): Promise<void> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
process.env.KEYPAIR || 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],
{
idsSource: 'get-program-accounts',
prioritizationFee: 80000,
},
);
const liqor = await client.getMangoAccount(new PublicKey(MANGO_ACCOUNT_PK!));
const group = await client.getGroup(liqor.group);
const forceCloseTokenBank = group.getFirstBankByTokenIndex(TOKEN_INDEX);
const mangoAccountsWithBorrows = (
await client.getAllMangoAccounts(group)
).filter((a) => a.getTokenBalanceUi(forceCloseTokenBank) < 0);
for (const liqee of mangoAccountsWithBorrows) {
console.log(
`liqee ${liqee.publicKey}, ${liqee.getTokenBalanceUi(
forceCloseTokenBank,
)}`,
);
const sig = await client.tokenDeposit(
group,
liqee,
forceCloseTokenBank.mint,
-liqee.getTokenBalanceUi(forceCloseTokenBank),
);
console.log(
` - tokendeposit, sig https://explorer.solana.com/tx/${
sig.signature
}?cluster=${CLUSTER == 'devnet' ? 'devnet' : ''}`,
);
}
}
tokenDeposit();

View File

@ -30,12 +30,10 @@ import { MANGO_V4_MAIN_GROUP as MANGO_V4_PRIMARY_GROUP } from '../src/constants'
import {
LiqorPriceImpact,
buildGroupGrid,
findLargestAssetBatchUi,
getEquityForMangoAccounts,
} from '../src/risk';
import {
buildFetch,
toNative,
toNativeI80F48ForQuote,
toUiDecimalsForQuote,
} from '../src/utils';
@ -167,20 +165,20 @@ async function updateTokenParams(): Promise<void> {
}
// eslint-disable-next-line no-constant-condition
if (false) {
// Deposit limits header
console.log(
`${'name'.padStart(20)} ${'maxLiqBatchUi'.padStart(
15,
)} ${'maxLiqBatchUi'.padStart(15)} ${'sellImpact'.padStart(
12,
)}$ ${'pi %'.padStart(12)}% ${'aNDUi'.padStart(
20,
)}${'aNDQuoteUi'.padStart(20)} ${'uiDeposits'.padStart(
12,
)} ${'uiDeposits'.padStart(12)} ${'depositLimitsUi'.padStart(12)}`,
);
}
// if (false) {
// // Deposit limits header
// console.log(
// `${'name'.padStart(20)} ${'maxLiqBatchUi'.padStart(
// 15,
// )} ${'maxLiqBatchUi'.padStart(15)} ${'sellImpact'.padStart(
// 12,
// )}$ ${'pi %'.padStart(12)}% ${'aNDUi'.padStart(
// 20,
// )}${'aNDQuoteUi'.padStart(20)} ${'uiDeposits'.padStart(
// 12,
// )} ${'uiDeposits'.padStart(12)} ${'depositLimitsUi'.padStart(12)}`,
// );
// }
Array.from(group.banksMapByTokenIndex.values())
.map((banks) => banks[0])
@ -189,266 +187,270 @@ async function updateTokenParams(): Promise<void> {
const builder = Builder(NullTokenEditParams);
let change = false;
try {
const tier = Object.values(LISTING_PRESETS).find((x) =>
x.initLiabWeight.toFixed(1) === '1.8'
? x.initLiabWeight.toFixed(1) ===
bank?.initLiabWeight.toNumber().toFixed(1) &&
x.reduceOnly === bank.reduceOnly
: x.initLiabWeight.toFixed(1) ===
bank?.initLiabWeight.toNumber().toFixed(1),
const tier = Object.values(LISTING_PRESETS).find((x) =>
x.initLiabWeight.toFixed(1) === '1.8'
? x.initLiabWeight.toFixed(1) ===
bank?.initLiabWeight.toNumber().toFixed(1) &&
x.reduceOnly === bank.reduceOnly
: x.initLiabWeight.toFixed(1) ===
bank?.initLiabWeight.toNumber().toFixed(1),
);
if (!tier) {
console.log(`Cant estimate tier for ${bank.name}!`);
return;
}
// eslint-disable-next-line no-constant-condition
// if (true) {
// if (
// bank.uiBorrows() == 0 &&
// bank.reduceOnly == 2 &&
// bank.initAssetWeight.toNumber() == 0 &&
// bank.maintAssetWeight.toNumber() == 0
// ) {
// builder.disableAssetLiquidation(true);
// builder.oracleConfig({
// confFilter: 1000,
// maxStalenessSlots: -1,
// });
// change = true;
// }
// }
// // eslint-disable-next-line no-constant-condition
// if (true) {
// if (bank.uiBorrows() == 0 && bank.reduceOnly == 1) {
// builder.disableAssetLiquidation(true);
// builder.forceWithdraw(true);
// change = true;
// }
// }
// // eslint-disable-next-line no-constant-condition
// if (true) {
// if (!tier) {
// console.log(`${bank.name}, no tier found`);
// } else if (tier.preset_name != 'C') {
// if (tier.preset_name.includes('A')) {
// builder.liquidationFee(bank.liquidationFee.toNumber() * 0.2);
// builder.platformLiquidationFee(
// bank.liquidationFee.toNumber() * 0.8,
// );
// } else if (tier.preset_name.includes('B')) {
// builder.liquidationFee(bank.liquidationFee.toNumber() * 0.4);
// builder.platformLiquidationFee(
// bank.liquidationFee.toNumber() * 0.6,
// );
// }
// change = true;
// }
// }
// eslint-disable-next-line no-constant-condition
// if (true) {
// if (!tier) {
// console.log(`${bank.name}, no tier found`);
// } else {
// console.log(
// `${bank.name.padStart(10)}, ${bank.loanFeeRate
// .mul(I80F48.fromNumber(100))
// .toFixed(2)}, ${bank.loanOriginationFeeRate
// .mul(I80F48.fromNumber(100))
// .toFixed(2)}, ${tier?.preset_name.padStart(5)}, ${(
// tier.loanFeeRate * 100
// ).toFixed(2)}, ${(tier!.loanOriginationFeeRate * 100).toFixed(2)}`,
// );
// builder.loanFeeRate(tier!.loanFeeRate);
// builder.loanOriginationFeeRate(tier!.loanOriginationFeeRate);
// builder.flashLoanSwapFeeRate(tier!.loanOriginationFeeRate);
// change = true;
// }
// }
// formulas are sourced from here
// https://www.notion.so/mango-markets/Mango-v4-Risk-parameter-recommendations-d309cdf5faac4aeea7560356e68532ab
// const priceImpact = getPriceImpactForBank(midPriceImpacts, bank);
// const scaleStartQuoteUi = Math.min(
// 50 * ttlLiqorEquityUi,
// 4 * priceImpact.target_amount,
// );
// eslint-disable-next-line no-constant-condition
if (!bank.areBorrowsReduceOnly()) {
// Net borrow limits
let netBorrowLimitPerWindowQuote = Math.max(
toUiDecimalsForQuote(tier!.netBorrowLimitPerWindowQuote),
Math.min(bank.uiDeposits() * bank.uiPrice, 300_000) / 3 +
Math.max(0, bank.uiDeposits() * bank.uiPrice - 300_000) / 5,
);
// eslint-disable-next-line no-constant-condition
if (true) {
if (
bank.uiBorrows() == 0 &&
bank.reduceOnly == 2 &&
bank.initAssetWeight.toNumber() == 0 &&
bank.maintAssetWeight.toNumber() == 0
) {
builder.disableAssetLiquidation(true);
builder.oracleConfig({
confFilter: 1000,
maxStalenessSlots: -1,
});
change = true;
}
}
// // eslint-disable-next-line no-constant-condition
// if (true) {
// if (bank.uiBorrows() == 0 && bank.reduceOnly == 1) {
// builder.disableAssetLiquidation(true);
// builder.forceWithdraw(true);
// change = true;
// }
// }
// // eslint-disable-next-line no-constant-condition
// if (true) {
// if (!tier) {
// console.log(`${bank.name}, no tier found`);
// } else if (tier.preset_name != 'C') {
// if (tier.preset_name.includes('A')) {
// builder.liquidationFee(bank.liquidationFee.toNumber() * 0.2);
// builder.platformLiquidationFee(
// bank.liquidationFee.toNumber() * 0.8,
// );
// } else if (tier.preset_name.includes('B')) {
// builder.liquidationFee(bank.liquidationFee.toNumber() * 0.4);
// builder.platformLiquidationFee(
// bank.liquidationFee.toNumber() * 0.6,
// );
// }
// change = true;
// }
// }
// eslint-disable-next-line no-constant-condition
// if (true) {
// if (!tier) {
// console.log(`${bank.name}, no tier found`);
// } else {
// console.log(
// `${bank.name.padStart(10)}, ${bank.loanFeeRate
// .mul(I80F48.fromNumber(100))
// .toFixed(2)}, ${bank.loanOriginationFeeRate
// .mul(I80F48.fromNumber(100))
// .toFixed(2)}, ${tier?.preset_name.padStart(5)}, ${(
// tier.loanFeeRate * 100
// ).toFixed(2)}, ${(tier!.loanOriginationFeeRate * 100).toFixed(2)}`,
// );
// builder.loanFeeRate(tier!.loanFeeRate);
// builder.loanOriginationFeeRate(tier!.loanOriginationFeeRate);
// builder.flashLoanSwapFeeRate(tier!.loanOriginationFeeRate);
// change = true;
// }
// }
// formulas are sourced from here
// https://www.notion.so/mango-markets/Mango-v4-Risk-parameter-recommendations-d309cdf5faac4aeea7560356e68532ab
// const priceImpact = getPriceImpactForBank(midPriceImpacts, bank);
// const scaleStartQuoteUi = Math.min(
// 50 * ttlLiqorEquityUi,
// 4 * priceImpact.target_amount,
// );
// eslint-disable-next-line no-constant-condition
if (false) {
// Net borrow limits
const netBorrowLimitPerWindowQuote = Math.max(
10_000,
Math.min(bank.uiDeposits() * bank.uiPrice, 300_000) / 3 +
Math.max(0, bank.uiDeposits() * bank.uiPrice - 300_000) / 5,
);
builder.netBorrowLimitPerWindowQuote(
toNativeI80F48ForQuote(netBorrowLimitPerWindowQuote).toNumber(),
);
change = true;
if (
netBorrowLimitPerWindowQuote !=
toUiDecimalsForQuote(bank.netBorrowLimitPerWindowQuote)
) {
console.log(
`${
bank.name
} new - ${netBorrowLimitPerWindowQuote.toLocaleString()}, old - ${toUiDecimalsForQuote(
bank.netBorrowLimitPerWindowQuote,
).toLocaleString()}`,
);
}
}
// Deposit limits
// eslint-disable-next-line no-constant-condition
if (false) {
if (bank.maintAssetWeight.toNumber() > 0) {
{
// Find asset's largest batch in $ we would need to liquidate, batches are extreme points of a range of price drop,
// range is constrained by leverage provided
// i.e. how much volatility we expect
const r = findLargestAssetBatchUi(
pisForLiqor,
bank.name,
Math.round(bank.maintAssetWeight.toNumber() * 100),
100 - Math.round(bank.maintAssetWeight.toNumber() * 100),
stepSize,
);
const maxLiqBatchQuoteUi = r[0];
const maxLiqBatchUi = r[1];
const sellImpact = getPriceImpactForBank(
midPriceImpacts,
bank,
(bank.liquidationFee.toNumber() * 100) / 2,
);
// Deposit limit = sell impact - largest batch
const allowedNewDepositsQuoteUi =
sellImpact.target_amount - maxLiqBatchQuoteUi;
const allowedNewDepositsUi =
sellImpact.target_amount / bank.uiPrice -
maxLiqBatchQuoteUi / bank.uiPrice;
const depositLimitUi = bank.uiDeposits() + allowedNewDepositsUi;
// LOG
// console.log(
// `${bank.name.padStart(20)} ${maxLiqBatchUi
// .toLocaleString()
// .padStart(15)} ${maxLiqBatchQuoteUi
// .toLocaleString()
// .padStart(15)}$ ${sellImpact.target_amount
// .toLocaleString()
// .padStart(12)}$ ${sellImpact.avg_price_impact_percent
// .toLocaleString()
// .padStart(12)}% ${allowedNewDepositsUi
// .toLocaleString()
// .padStart(20)}${allowedNewDepositsQuoteUi
// .toLocaleString()
// .padStart(20)}$ ${bank
// .uiDeposits()
// .toLocaleString()
// .padStart(12)} ${(bank.uiDeposits() * bank.uiPrice)
// .toLocaleString()
// .padStart(12)}$ ${depositLimitUi
// .toLocaleString()
// .padStart(12)}`,
// );
builder.depositLimit(toNative(depositLimitUi, bank.mintDecimals));
change = true;
}
}
}
const params = builder.build();
console.log(
`${bank.name}, ${params.disableAssetLiquidation} ${params.oracleConfig?.maxStalenessSlots} ${params.oracleConfig?.confFilter}`,
netBorrowLimitPerWindowQuote =
Math.round(netBorrowLimitPerWindowQuote / 10_000) * 10_000;
builder.netBorrowLimitPerWindowQuote(
toNativeI80F48ForQuote(netBorrowLimitPerWindowQuote).toNumber(),
);
const ix = await client.program.methods
.tokenEdit(
params.oracle,
params.oracleConfig,
params.groupInsuranceFund,
params.interestRateParams,
params.loanFeeRate,
params.loanOriginationFeeRate,
params.maintAssetWeight,
params.initAssetWeight,
params.maintLiabWeight,
params.initLiabWeight,
params.liquidationFee,
params.stablePriceDelayIntervalSeconds,
params.stablePriceDelayGrowthLimit,
params.stablePriceGrowthLimit,
params.minVaultToDepositsRatio,
params.netBorrowLimitPerWindowQuote !== null
? new BN(params.netBorrowLimitPerWindowQuote)
: null,
params.netBorrowLimitWindowSizeTs !== null
? new BN(params.netBorrowLimitWindowSizeTs)
: null,
params.borrowWeightScaleStartQuote,
params.depositWeightScaleStartQuote,
params.resetStablePrice ?? false,
params.resetNetBorrowLimit ?? false,
params.reduceOnly,
params.name,
params.forceClose,
params.tokenConditionalSwapTakerFeeRate,
params.tokenConditionalSwapMakerFeeRate,
params.flashLoanSwapFeeRate,
params.interestCurveScaling,
params.interestTargetUtilization,
params.maintWeightShiftStart,
params.maintWeightShiftEnd,
params.maintWeightShiftAssetTarget,
params.maintWeightShiftLiabTarget,
params.maintWeightShiftAbort ?? false,
false, // setFallbackOracle, unused
params.depositLimit,
params.zeroUtilRate,
params.platformLiquidationFee,
params.disableAssetLiquidation,
params.collateralFeePerDay,
params.forceWithdraw,
)
.accounts({
group: group.publicKey,
oracle: bank.oracle,
admin: group.admin,
mintInfo: group.mintInfosMapByTokenIndex.get(bank.tokenIndex)
?.publicKey,
fallbackOracle: PublicKey.default,
})
.remainingAccounts([
{
pubkey: bank.publicKey,
isWritable: true,
isSigner: false,
} as AccountMeta,
])
.instruction();
const tx = new Transaction({ feePayer: wallet.publicKey }).add(ix);
const simulated = await client.connection.simulateTransaction(tx);
if (simulated.value.err) {
console.log('error', simulated.value.logs);
throw simulated.value.logs;
change = true;
if (
netBorrowLimitPerWindowQuote !=
toUiDecimalsForQuote(bank.netBorrowLimitPerWindowQuote)
) {
console.log(
`${bank.name}, ${bank.uiDeposits() * bank.uiPrice}$ (${
Math.min(bank.uiDeposits() * bank.uiPrice, 300_000) / 3
}, ${
Math.max(0, bank.uiDeposits() * bank.uiPrice - 300_000) / 5
}), ${
tier?.netBorrowLimitPerWindowQuote
} , new - ${netBorrowLimitPerWindowQuote}, old - ${toUiDecimalsForQuote(
bank.netBorrowLimitPerWindowQuote,
)}`,
);
}
}
if (change) {
instructions.push(ix);
}
} catch (error) {
console.log(`....Skipping ${bank.name}, ${error}`);
// Deposit limits
// eslint-disable-next-line no-constant-condition
// if (false) {
// if (bank.maintAssetWeight.toNumber() > 0) {
// {
// // Find asset's largest batch in $ we would need to liquidate, batches are extreme points of a range of price drop,
// // range is constrained by leverage provided
// // i.e. how much volatility we expect
// const r = findLargestAssetBatchUi(
// pisForLiqor,
// bank.name,
// Math.round(bank.maintAssetWeight.toNumber() * 100),
// 100 - Math.round(bank.maintAssetWeight.toNumber() * 100),
// stepSize,
// );
// const maxLiqBatchQuoteUi = r[0];
// const maxLiqBatchUi = r[1];
// const sellImpact = getPriceImpactForBank(
// midPriceImpacts,
// bank,
// (bank.liquidationFee.toNumber() * 100) / 2,
// );
// // Deposit limit = sell impact - largest batch
// const allowedNewDepositsQuoteUi =
// sellImpact.target_amount - maxLiqBatchQuoteUi;
// const allowedNewDepositsUi =
// sellImpact.target_amount / bank.uiPrice -
// maxLiqBatchQuoteUi / bank.uiPrice;
// const depositLimitUi = bank.uiDeposits() + allowedNewDepositsUi;
// // LOG
// // console.log(
// // `${bank.name.padStart(20)} ${maxLiqBatchUi
// // .toLocaleString()
// // .padStart(15)} ${maxLiqBatchQuoteUi
// // .toLocaleString()
// // .padStart(15)}$ ${sellImpact.target_amount
// // .toLocaleString()
// // .padStart(12)}$ ${sellImpact.avg_price_impact_percent
// // .toLocaleString()
// // .padStart(12)}% ${allowedNewDepositsUi
// // .toLocaleString()
// // .padStart(20)}${allowedNewDepositsQuoteUi
// // .toLocaleString()
// // .padStart(20)}$ ${bank
// // .uiDeposits()
// // .toLocaleString()
// // .padStart(12)} ${(bank.uiDeposits() * bank.uiPrice)
// // .toLocaleString()
// // .padStart(12)}$ ${depositLimitUi
// // .toLocaleString()
// // .padStart(12)}`,
// // );
// builder.depositLimit(toNative(depositLimitUi, bank.mintDecimals));
// change = true;
// }
// }
// }
const params = builder.build();
const ix = await client.program.methods
.tokenEdit(
params.oracle,
params.oracleConfig,
params.groupInsuranceFund,
params.interestRateParams,
params.loanFeeRate,
params.loanOriginationFeeRate,
params.maintAssetWeight,
params.initAssetWeight,
params.maintLiabWeight,
params.initLiabWeight,
params.liquidationFee,
params.stablePriceDelayIntervalSeconds,
params.stablePriceDelayGrowthLimit,
params.stablePriceGrowthLimit,
params.minVaultToDepositsRatio,
params.netBorrowLimitPerWindowQuote !== null
? new BN(params.netBorrowLimitPerWindowQuote)
: null,
params.netBorrowLimitWindowSizeTs !== null
? new BN(params.netBorrowLimitWindowSizeTs)
: null,
params.borrowWeightScaleStartQuote,
params.depositWeightScaleStartQuote,
params.resetStablePrice ?? false,
params.resetNetBorrowLimit ?? false,
params.reduceOnly,
params.name,
params.forceClose,
params.tokenConditionalSwapTakerFeeRate,
params.tokenConditionalSwapMakerFeeRate,
params.flashLoanSwapFeeRate,
params.interestCurveScaling,
params.interestTargetUtilization,
params.maintWeightShiftStart,
params.maintWeightShiftEnd,
params.maintWeightShiftAssetTarget,
params.maintWeightShiftLiabTarget,
params.maintWeightShiftAbort ?? false,
false, // setFallbackOracle, unused
params.depositLimit,
params.zeroUtilRate,
params.platformLiquidationFee,
params.disableAssetLiquidation,
params.collateralFeePerDay,
params.forceWithdraw,
)
.accounts({
group: group.publicKey,
oracle: bank.oracle,
admin: group.admin,
mintInfo: group.mintInfosMapByTokenIndex.get(bank.tokenIndex)
?.publicKey,
fallbackOracle: PublicKey.default,
})
.remainingAccounts([
{
pubkey: bank.publicKey,
isWritable: true,
isSigner: false,
} as AccountMeta,
])
.instruction();
const tx = new Transaction({ feePayer: wallet.publicKey }).add(ix);
const simulated = await client.connection.simulateTransaction(tx);
if (simulated.value.err) {
console.log('error', simulated.value.logs);
throw simulated.value.logs;
}
if (change) {
instructions.push(ix);
}
});
@ -470,8 +472,6 @@ async function updateTokenParams(): Promise<void> {
const walletSigner = wallet as never;
console.log(DRY_RUN);
if (!DRY_RUN) {
const proposalAddress = await createProposal(
client.connection,
@ -480,7 +480,7 @@ async function updateTokenParams(): Promise<void> {
tokenOwnerRecord,
PROPOSAL_TITLE
? PROPOSAL_TITLE
: 'Disable asset liquidation for C tier tokens in mango-v4, part 2',
: 'Update net borrow limits for tokens in mango-v4',
PROPOSAL_LINK ?? '',
Object.values(proposals).length,
instructions,

View File

@ -1,18 +1,12 @@
import { PublicKey } from '@solana/web3.js';
import {
HealthType,
MangoAccount,
TokenPosition,
TokenPositionDto,
} from './mangoAccount';
import BN from 'bn.js';
import { Bank, TokenIndex } from './bank';
import { deepClone, toNative, toUiDecimals } from '../utils';
import { expect } from 'chai';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { Group } from './group';
import { HealthCache } from './healthCache';
import { assert } from 'console';
import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { deepClone, toNative, toUiDecimals } from '../utils';
import { Bank, TokenIndex } from './bank';
import { Group } from './group';
import { MangoAccount, TokenPosition } from './mangoAccount';
describe('Mango Account', () => {
const mangoAccount = new MangoAccount(
@ -33,6 +27,7 @@ describe('Mango Account', () => {
new BN(0),
0,
0,
new BN(0),
[],
[],
[],
@ -109,6 +104,7 @@ describe('maxWithdraw', () => {
new BN(0),
0,
0,
new BN(0),
[],
[],
[],

View File

@ -62,6 +62,7 @@ export class MangoAccount {
perps: unknown;
perpOpenOrders: unknown;
tokenConditionalSwaps: unknown;
lastCollateralFeeCharge: BN;
},
): MangoAccount {
return new MangoAccount(
@ -82,6 +83,7 @@ export class MangoAccount {
obj.buybackFeesExpiryTimestamp,
obj.sequenceNumber,
obj.headerVersion,
obj.lastCollateralFeeCharge,
obj.tokens as TokenPositionDto[],
obj.serum3 as Serum3PositionDto[],
obj.openbookV2 as OpenbookV2PositionDto[],
@ -111,6 +113,7 @@ export class MangoAccount {
public buybackFeesExpiryTimestamp: BN,
public sequenceNumber: number,
public headerVersion: number,
public lastCollateralFeeCharge: BN,
tokens: TokenPositionDto[],
serum3: Serum3PositionDto[],
openbookV2: OpenbookV2PositionDto[],
@ -1762,12 +1765,10 @@ export class PerpPosition {
throw new Error("PerpPosition doesn't belong to the given market!");
}
const cumulativeFunding = this.getCumulativeFunding(perpMarket);
// can't be long and short at the same time
if (cumulativeFunding.cumulativeLongFunding !== 0) {
return -1 * toUiDecimalsForQuote(cumulativeFunding.cumulativeLongFunding);
} else {
return toUiDecimalsForQuote(cumulativeFunding.cumulativeShortFunding);
}
return (
-1 * toUiDecimalsForQuote(cumulativeFunding.cumulativeLongFunding) +
toUiDecimalsForQuote(cumulativeFunding.cumulativeShortFunding)
);
}
public getEquity(perpMarket: PerpMarket): I80F48 {

View File

@ -0,0 +1,118 @@
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { expect } from 'chai';
import {
USDC_MINT_MAINNET,
deriveFallbackOracleQuoteKey,
isOrcaOracle,
isRaydiumOracle,
} from './oracle';
import { MangoClient } from '../client';
import { MANGO_V4_ID, MANGO_V4_MAIN_GROUP } from '../constants';
import { AnchorProvider, Provider, Wallet } from '@coral-xyz/anchor';
import * as fs from 'fs';
import { Group } from './group';
function getProvider(connection: Connection): Provider {
const secretKey = JSON.parse(
fs.readFileSync(process.env.KEYPAIR_PATH as string, 'utf-8'),
);
const kp = Keypair.fromSecretKey(Uint8Array.from(secretKey));
const wallet = new Wallet(kp);
const provider = new AnchorProvider(connection, wallet, {});
return provider;
}
describe.only('Oracle', () => {
const connection = new Connection('https://api.mainnet-beta.solana.com/');
const CLUSTER = 'mainnet-beta';
const Orca_SOL_USDC_Whirlpool = new PublicKey(
'83v8iPyZihDEjDdY8RdZddyZNyUtXngz69Lgo9Kt5d6d',
);
const Raydium_SOL_USDC_Whirlpool = new PublicKey(
'Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr',
);
it('can decode Orca CLMM oracles', async () => {
const accInfo = await connection.getAccountInfo(Orca_SOL_USDC_Whirlpool);
expect(accInfo).not.to.be.null;
expect(isOrcaOracle(accInfo!)).to.be.true;
const other = await connection.getAccountInfo(Raydium_SOL_USDC_Whirlpool);
expect(isOrcaOracle(other!)).to.be.false;
const quoteKey = deriveFallbackOracleQuoteKey(accInfo!);
expect(quoteKey.equals(USDC_MINT_MAINNET)).to.be.true;
});
it('can decode Raydium CLMM oracles', async () => {
const accInfo = await connection.getAccountInfo(Raydium_SOL_USDC_Whirlpool);
expect(accInfo).not.to.be.null;
expect(isRaydiumOracle(accInfo!)).to.be.true;
const other = await connection.getAccountInfo(Orca_SOL_USDC_Whirlpool);
expect(isRaydiumOracle(other!)).to.be.false;
const quoteKey = deriveFallbackOracleQuoteKey(accInfo!);
expect(quoteKey.equals(USDC_MINT_MAINNET)).to.be.true;
});
it.skip('can generate fixed fallback oracles', async () => {
const provider = getProvider(connection);
const client = MangoClient.connect(
provider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{
fallbackOracleConfig: [
new PublicKey('H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG'),
],
},
); // SOL
const groupAccount = await client.program.account.group.fetch(
MANGO_V4_MAIN_GROUP,
);
const GROUP = Group.from(MANGO_V4_MAIN_GROUP, groupAccount);
await GROUP.reloadBanks(client);
const fbs = await client.deriveFallbackOracleContexts(GROUP);
expect(fbs.size).to.equal(1);
});
it.skip('can generate all fallback oracles', async () => {
const provider = getProvider(connection);
const client = MangoClient.connect(
provider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{ fallbackOracleConfig: 'all' },
);
const groupAccount = await client.program.account.group.fetch(
MANGO_V4_MAIN_GROUP,
);
const GROUP = Group.from(MANGO_V4_MAIN_GROUP, groupAccount);
await GROUP.reloadBanks(client);
const fbs = await client.deriveFallbackOracleContexts(GROUP);
expect(fbs.size).to.be.greaterThan(1);
});
it.skip('can generate dynamic fallback oracles', async () => {
const provider = getProvider(connection);
const client = MangoClient.connect(
provider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{ fallbackOracleConfig: 'dynamic' },
);
const groupAccount = await client.program.account.group.fetch(
MANGO_V4_MAIN_GROUP,
);
const GROUP = Group.from(MANGO_V4_MAIN_GROUP, groupAccount);
await GROUP.reloadBanks(client);
const fbs = await client.deriveFallbackOracleContexts(GROUP);
console.log(fbs.size);
});
});

View File

@ -11,6 +11,26 @@ const SBV1_DEVNET_PID = new PublicKey(
const SBV1_MAINNET_PID = new PublicKey(
'DtmE9D2CSB4L5D6A15mraeEjrGMm6auWVzgaD8hK2tZM',
);
const ORCA_MAINNET_PID = new PublicKey(
'whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc',
);
const ORCA_WHIRLPOOL_LEN = 653;
const ORCA_WHIRLPOOL_DISCRIMINATOR = [63, 149, 209, 12, 225, 128, 99, 9];
const RAYDIUM_MAINNET_PID = new PublicKey(
'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK',
);
const RAYDIUM_POOL_LEN = 1544;
const RAYDIUM_POOL_DISCRIMINATOR = [247, 237, 227, 245, 215, 195, 222, 70];
export const USDC_MINT_MAINNET = new PublicKey(
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
);
export const SOL_MINT_MAINNET = new PublicKey(
'So11111111111111111111111111111111111111112',
);
let sbv2DevnetProgram;
let sbv2MainnetProgram;
@ -164,6 +184,36 @@ export function isPythOracle(accountInfo: AccountInfo<Buffer>): boolean {
return accountInfo.data.readUInt32LE(0) === PythMagic;
}
export function isOrcaOracle(accountInfo: AccountInfo<Buffer>): boolean {
for (let i = 0; i < 8; i++) {
if (accountInfo.data.at(i) !== ORCA_WHIRLPOOL_DISCRIMINATOR[i]) {
return false;
}
}
return (
accountInfo.owner.equals(ORCA_MAINNET_PID) &&
accountInfo.data.length == ORCA_WHIRLPOOL_LEN
);
}
export function isRaydiumOracle(accountInfo: AccountInfo<Buffer>): boolean {
for (let i = 0; i < 8; i++) {
if (accountInfo.data.at(i) !== RAYDIUM_POOL_DISCRIMINATOR[i]) {
return false;
}
}
return (
accountInfo.owner.equals(RAYDIUM_MAINNET_PID) &&
accountInfo.data.length == RAYDIUM_POOL_LEN
);
}
export function isClmmOracle(accountInfo: AccountInfo<Buffer>): boolean {
return isOrcaOracle(accountInfo) || isRaydiumOracle(accountInfo);
}
export function isOracleStaleOrUnconfident(
nowSlot: number,
maxStalenessSlots: number,
@ -186,3 +236,50 @@ export function isOracleStaleOrUnconfident(
return false;
}
export function deriveFallbackOracleQuoteKey(
accountInfo: AccountInfo<Buffer>,
): PublicKey {
if (isOrcaOracle(accountInfo)) {
const tokenA = new PublicKey(accountInfo.data.subarray(101, 133));
const tokenB = new PublicKey(accountInfo.data.subarray(181, 213));
return clmmQuoteKey(tokenA, tokenB);
} else if (isRaydiumOracle(accountInfo)) {
const tokenA = new PublicKey(accountInfo.data.subarray(73, 105));
const tokenB = new PublicKey(accountInfo.data.subarray(105, 137));
return clmmQuoteKey(tokenA, tokenB);
} else {
return PublicKey.default;
}
}
function clmmQuoteKey(tokenA: PublicKey, tokenB: PublicKey): PublicKey {
if (
tokenA.equals(USDC_MINT_MAINNET) ||
(tokenA.equals(SOL_MINT_MAINNET) && !tokenB.equals(USDC_MINT_MAINNET))
) {
return tokenA; // inverted
} else {
return tokenB;
}
}
// Assumes oracles.length === fallbacks.length
export async function createFallbackOracleMap(
conn: Connection,
oracles: PublicKey[],
fallbacks: PublicKey[],
): Promise<Map<string, [PublicKey, PublicKey]>> {
const map: Map<string, [PublicKey, PublicKey]> = new Map();
const accounts = await conn.getMultipleAccountsInfo(fallbacks);
for (let i = 0; i < oracles.length; i++) {
if (accounts[i] === null) {
map.set(oracles[i].toBase58(), [fallbacks[i], PublicKey.default]);
} else if (!isClmmOracle(accounts[i]!)) {
map.set(oracles[i].toBase58(), [fallbacks[i], PublicKey.default]);
} else {
const quoteKey = deriveFallbackOracleQuoteKey(accounts[i]!);
map.set(oracles[i].toBase58(), [fallbacks[i], quoteKey]);
}
}
return map;
}

View File

@ -59,7 +59,7 @@ import {
generateOpenbookV2MarketExternalVaultSignerAddress,
priceNumberToLots,
} from './accounts/openbookV2';
import { StubOracle } from './accounts/oracle';
import { StubOracle, createFallbackOracleMap } from './accounts/oracle';
import {
FillEvent,
OutEvent,
@ -123,6 +123,7 @@ export enum AccountRetriever {
}
export type IdsSource = 'api' | 'static' | 'get-program-accounts';
export type FallbackOracleConfig = 'never' | 'all' | 'dynamic' | PublicKey[]; // 'fixed'
export type MangoClientOptions = {
idsSource?: IdsSource;
@ -134,6 +135,7 @@ export type MangoClientOptions = {
openbookFeesToDao?: boolean;
prependedGlobalAdditionalInstructions?: TransactionInstruction[];
multipleConnections?: Connection[];
fallbackOracleConfig?: FallbackOracleConfig;
};
export type TxCallbackOptions = {
@ -150,6 +152,8 @@ export class MangoClient {
private txConfirmationCommitment: Commitment;
private openbookFeesToDao: boolean;
private prependedGlobalAdditionalInstructions: TransactionInstruction[] = [];
private fallbackOracleConfig: FallbackOracleConfig = 'never';
private fixedFallbacks: Map<string, [PublicKey, PublicKey]> = new Map();
multipleConnections: Connection[] = [];
constructor(
@ -173,6 +177,7 @@ export class MangoClient {
// TODO: evil side effect, but limited backtraces are a nightmare
Error.stackTraceLimit = 1000;
this.multipleConnections = opts?.multipleConnections ?? [];
this.fallbackOracleConfig = opts?.fallbackOracleConfig ?? 'never';
}
/// Convenience accessors
@ -618,7 +623,7 @@ export class MangoClient {
const assetBank = group.getFirstBankByTokenIndex(assetTokenIndex);
const liabBank = group.getFirstBankByTokenIndex(liabTokenIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[liqor, liqee],
[assetBank, liabBank],
@ -1135,7 +1140,13 @@ export class MangoClient {
checkKind: HealthCheckKind,
): Promise<TransactionInstruction> {
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(group, [mangoAccount], [], [], []);
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
[],
[],
[],
);
return await this.program.methods
.healthCheck(minHealthValue, checkKind)
@ -1690,7 +1701,12 @@ export class MangoClient {
}
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(group, [mangoAccount], [bank], []);
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
[bank],
[],
);
const sharedAccounts = {
group: group.publicKey,
@ -1846,7 +1862,13 @@ export class MangoClient {
}
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(group, [mangoAccount], [bank], [], []);
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
[bank],
[],
[],
);
const ix = await this.program.methods
.tokenWithdraw(new BN(nativeAmount), allowBorrow)
@ -2170,7 +2192,7 @@ export class MangoClient {
);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
[],
@ -2283,7 +2305,7 @@ export class MangoClient {
}
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
banks,
@ -2407,7 +2429,7 @@ export class MangoClient {
}
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
banks,
@ -3062,7 +3084,7 @@ export class MangoClient {
}
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
[],
@ -3148,7 +3170,7 @@ export class MangoClient {
}
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
banks,
@ -3735,7 +3757,7 @@ export class MangoClient {
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(group, [mangoAccount], [], []);
await this.buildHealthRemainingAccounts(group, [mangoAccount], [], []);
return await this.program.methods
.perpDeactivatePosition()
.accounts({
@ -3868,7 +3890,7 @@ export class MangoClient {
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
// Settlement token bank, because a position for it may be created
@ -3925,7 +3947,7 @@ export class MangoClient {
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
// Settlement token bank, because a position for it may be created
@ -4018,7 +4040,7 @@ export class MangoClient {
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
// Settlement token bank, because a position for it may be created
@ -4078,7 +4100,7 @@ export class MangoClient {
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
// Settlement token bank, because a position for it may be created
@ -4360,7 +4382,7 @@ export class MangoClient {
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[profitableAccount, unprofitableAccount],
[group.getFirstBankForPerpSettlement()],
@ -4414,7 +4436,7 @@ export class MangoClient {
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[account], // Account must be unprofitable
[group.getFirstBankForPerpSettlement()],
@ -4557,7 +4579,7 @@ export class MangoClient {
const outputBank: Bank = group.getFirstBankByMint(outputMintPk);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
[inputBank, outputBank],
@ -4753,7 +4775,7 @@ export class MangoClient {
const liabBank: Bank = group.getFirstBankByMint(liabMintPk);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[liqor, liqee],
[assetBank, liabBank],
@ -5747,7 +5769,7 @@ export class MangoClient {
const sellBank = group.banksMapByTokenIndex.get(tcs.sellTokenIndex)![0];
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[liqor, liqee],
[buyBank, sellBank],
@ -5826,7 +5848,7 @@ export class MangoClient {
perpMarkets: PerpMarket[] = [],
): Promise<TransactionInstruction> {
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[account],
[...banks],
@ -5859,7 +5881,7 @@ export class MangoClient {
perpMarkets: PerpMarket[] = [],
): Promise<TransactionInstruction> {
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
await this.buildHealthRemainingAccounts(
group,
[account],
[...banks],
@ -5890,6 +5912,7 @@ export class MangoClient {
opts?: MangoClientOptions,
): MangoClient {
const idl = IDL;
console.log(opts);
return new MangoClient(
new Program<MangoV4>(idl as MangoV4, programId, provider),
@ -5961,10 +5984,11 @@ export class MangoClient {
* @param mangoAccounts
* @param banks - banks in which new positions might be opened
* @param perpMarkets - markets in which new positions might be opened
* @param openOrdersForMarket - markets in which new positions might be opened
* @param serumOpenOrdersForMarket - markets in which new positions might be opened (openbook v1)
* @param openbookOpenOrdersForMarket - markets in which new positions might be opened (openbook v2)
* @returns
*/
buildHealthRemainingAccounts(
async buildHealthRemainingAccounts(
group: Group,
mangoAccounts: MangoAccount[],
// Banks and markets for whom positions don't exist on mango account,
@ -5973,7 +5997,7 @@ export class MangoClient {
perpMarkets: PerpMarket[] = [],
serumOpenOrdersForMarket: [Serum3Market, PublicKey][] = [],
openbookOpenOrdersForMarket: [OpenbookV2Market, PublicKey][] = [],
): PublicKey[] {
): Promise<PublicKey[]> {
const healthRemainingAccounts: PublicKey[] = [];
const tokenPositionIndices = mangoAccounts
@ -6118,9 +6142,81 @@ export class MangoClient {
.map((openbookPosition) => openbookPosition.openOrders),
);
const fallbackMap = await this.deriveFallbackOracleContexts(group);
const fallbacks: PublicKey[] = [];
for (const oracle of mintInfos.map((mintInfo) => mintInfo.oracle)) {
if (fallbackMap.has(oracle.toBase58())) {
fallbacks.push(...fallbackMap.get(oracle.toBase58())!);
}
}
for (const fallback of uniq(fallbacks)) {
if (
!healthRemainingAccounts.find((h) => h.equals(fallback)) &&
!fallback.equals(PublicKey.default)
) {
healthRemainingAccounts.push(fallback);
}
}
return healthRemainingAccounts;
}
/**This function assumes that the provided group has loaded banks*/
public async deriveFallbackOracleContexts(
group: Group,
): Promise<Map<string, [PublicKey, PublicKey]>> {
// fixed
if (typeof this.fallbackOracleConfig !== 'string') {
if (this.fixedFallbacks.size === 0) {
const oracles: PublicKey[] = this.fallbackOracleConfig;
const fallbacks: PublicKey[] = [];
Array.from(group.banksMapByTokenIndex.values()).forEach((b) => {
if (oracles.find((o) => o.toBase58() === b[0].oracle.toBase58())) {
fallbacks.push(b[0].fallbackOracle);
}
});
this.fixedFallbacks = await createFallbackOracleMap(
this.connection,
oracles,
fallbacks,
);
}
return this.fixedFallbacks;
}
switch (this.fallbackOracleConfig) {
case 'never':
return new Map();
case 'dynamic': {
const nowSlot = await this.connection.getSlot();
const oracles: PublicKey[] = [];
const fallbacks: PublicKey[] = [];
await group.reloadBankOraclePrices(this);
Array.from(group.banksMapByTokenIndex.values())
.filter((b) => b[0].isOracleStaleOrUnconfident(nowSlot))
.forEach((b) => {
oracles.push(b[0].oracle);
fallbacks.push(b[0].fallbackOracle);
});
return createFallbackOracleMap(this.connection, oracles, fallbacks);
}
case 'all': {
const oracles: PublicKey[] = [];
const fallbacks: PublicKey[] = [];
Array.from(group.banksMapByTokenIndex.values()).forEach((b) => {
oracles.push(b[0].oracle);
fallbacks.push(b[0].fallbackOracle);
});
return createFallbackOracleMap(this.connection, oracles, fallbacks);
}
default: {
return new Map();
}
}
}
public async modifyPerpOrder(
group: Group,
mangoAccount: MangoAccount,

View File

@ -38,10 +38,10 @@
bn.js "^5.2.1"
eslint-config-prettier "^9.0.0"
"@blockworks-foundation/mangolana@0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mangolana/-/mangolana-0.0.14.tgz#f2a0e164f2cbe6e0a0db4fc10267e81ef5d2f7ec"
integrity sha512-KuA2+GdeKoHCBmx2HZnVb8IPomUP1w0ZiwQ1F10SLIypYfrylvPa+HSK2ak/+nzZCb8erS9Oub45UPV7cOh5ng==
"@blockworks-foundation/mangolana@0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mangolana/-/mangolana-0.0.15.tgz#8a603675028f7d7ab400cc58bf3058d2e7514cde"
integrity sha512-P31XW2w2lFBgzM+529H2f3Kz8rNC7+h0pYZub9kOcsACAt7TwLksg1fGuzSvekLn6M1JmVpPa9nISdAVfsZB5g==
dependencies:
"@solana/web3.js" "^1.88.0"
bs58 "^5.0.0"
@ -173,7 +173,7 @@
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
"@noble/hashes@^1.3.1", "@noble/hashes@^1.3.2", "@noble/hashes@^1.3.3":
"@noble/hashes@^1.3.1", "@noble/hashes@^1.3.3":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==