Merge branch 'dev'
This commit is contained in:
commit
5ff181d01d
|
@ -11,6 +11,11 @@ on:
|
|||
description: 'Docker Image Name'
|
||||
required: true
|
||||
type: string
|
||||
imageTag:
|
||||
description: 'Docker Image Tag'
|
||||
required: true
|
||||
type: string
|
||||
default: 'latest'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
@ -27,7 +32,7 @@ jobs:
|
|||
- name: Push
|
||||
env:
|
||||
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
|
||||
run: heroku container:push ${{ inputs.imageName }} -a ${{ inputs.appName }} --recursive
|
||||
run: heroku container:push ${{ inputs.imageName }} -a ${{ inputs.appName }} --recursive --arg BASE_TAG=${{ inputs.imageTag }}
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
|
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
# Use docker buildx
|
||||
- name: Use docker buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
@ -47,8 +47,8 @@ jobs:
|
|||
username: oauth2accesstoken
|
||||
password: ${{ steps.auth.outputs.access_token }}
|
||||
|
||||
# Build and push the image, leveraging layer caching
|
||||
- name: Build and Push
|
||||
# Build and push the base image, leveraging layer caching
|
||||
- name: Build and Push Base Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
|
@ -58,3 +58,23 @@ jobs:
|
|||
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:latest
|
||||
cache-from: type=registry,ref=us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:buildcache,mode=max
|
||||
# Build and push the liquidator runtime image
|
||||
- name: Build and Push Liquidator
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
file: liquidator/Dockerfile.liquidator
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-liquidator:${{ github.sha }}
|
||||
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-liquidator:latest
|
||||
# Build and push the keeper runtime image
|
||||
- name: Build and Push Keeper
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
file: keeper/Dockerfile.keeper
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-keeper:${{ github.sha }}
|
||||
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-keeper:latest
|
||||
|
|
|
@ -22,3 +22,5 @@ ts/client/**/*.js
|
|||
ts/client/**/*.js.map
|
||||
migrations/*.js
|
||||
migrations/*.js.map
|
||||
|
||||
ts/client/src/scripts/archive/ts.ts
|
|
@ -4,6 +4,9 @@ Update this for each mainnet deployment.
|
|||
|
||||
## not on mainnet
|
||||
|
||||
- Add HealthRegionBegin, -End instructions
|
||||
- Add explicit "oracle" account argument for TokenDeposit and TokenWithdraw instructions
|
||||
|
||||
## mainnet
|
||||
|
||||
Aug 20, 2022 at 19:58:29 Central European Summer Time
|
||||
|
|
|
@ -3208,6 +3208,7 @@ dependencies = [
|
|||
"fixed",
|
||||
"fixed-macro",
|
||||
"itertools 0.10.3",
|
||||
"lazy_static",
|
||||
"log 0.4.17",
|
||||
"mango-macro",
|
||||
"margin-trade",
|
||||
|
|
|
@ -9,12 +9,12 @@ COPY ./ .
|
|||
RUN sed -i 's|lib/\*|lib/checked_math|' Cargo.toml
|
||||
|
||||
# Mount cache for downloaded and compiled dependencies
|
||||
RUN --mount=type=cache,target=/usr/local/cargo,from=rust,source=/usr/local/cargo \
|
||||
--mount=type=cache,target=target \
|
||||
RUN --mount=type=cache,mode=0777,target=/usr/local/cargo,from=rust,source=/usr/local/cargo \
|
||||
--mount=type=cache,mode=0777,target=target \
|
||||
cargo build --release --bins
|
||||
|
||||
# Copy bins out of cache
|
||||
RUN --mount=type=cache,target=target mkdir .bin && cp target/release/keeper target/release/liquidator .bin/
|
||||
RUN --mount=type=cache,mode=0777,target=target mkdir .bin && cp target/release/keeper target/release/liquidator .bin/
|
||||
|
||||
FROM debian:bullseye-slim as run
|
||||
RUN apt-get update && apt-get -y install ca-certificates libc6
|
||||
|
|
|
@ -203,7 +203,7 @@ impl ChainData {
|
|||
.iter()
|
||||
.rev()
|
||||
.find(|w| self.is_account_write_live(w))?;
|
||||
Some((pubkey.clone(), latest_good_write.clone()))
|
||||
Some((*pubkey, latest_good_write.clone()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
|
@ -28,11 +28,10 @@ impl AccountFetcher {
|
|||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<T> {
|
||||
Ok(self
|
||||
Ok(*self
|
||||
.fetch_raw(address)?
|
||||
.load::<T>()
|
||||
.with_context(|| format!("loading account {}", address))?
|
||||
.clone())
|
||||
.with_context(|| format!("loading account {}", address))?)
|
||||
}
|
||||
|
||||
pub fn fetch_mango_account(&self, address: &Pubkey) -> anyhow::Result<MangoAccountValue> {
|
||||
|
@ -40,12 +39,12 @@ impl AccountFetcher {
|
|||
|
||||
let data = acc.data();
|
||||
let disc_bytes = &data[0..8];
|
||||
if disc_bytes != &MangoAccount::discriminator() {
|
||||
if disc_bytes != MangoAccount::discriminator() {
|
||||
anyhow::bail!("not a mango account at {}", address);
|
||||
}
|
||||
|
||||
Ok(MangoAccountValue::from_bytes(&data[8..])
|
||||
.with_context(|| format!("loading mango account {}", address))?)
|
||||
MangoAccountValue::from_bytes(&data[8..])
|
||||
.with_context(|| format!("loading mango account {}", address))
|
||||
}
|
||||
|
||||
// fetches via RPC, stores in ChainData, returns new version
|
||||
|
@ -73,7 +72,7 @@ impl AccountFetcher {
|
|||
pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
|
||||
let response = self
|
||||
.rpc
|
||||
.get_account_with_commitment(&address, self.rpc.commitment())
|
||||
.get_account_with_commitment(address, self.rpc.commitment())
|
||||
.with_context(|| format!("refresh account {} via rpc", address))?;
|
||||
let slot = response.context.slot;
|
||||
let account = response
|
||||
|
|
|
@ -374,6 +374,7 @@ impl MangoClient {
|
|||
account: self.mango_account_address,
|
||||
bank: mint_info.first_bank(),
|
||||
vault: mint_info.first_vault(),
|
||||
oracle: mint_info.oracle,
|
||||
token_account: get_associated_token_address(
|
||||
&self.owner(),
|
||||
&mint_info.mint,
|
||||
|
@ -425,6 +426,7 @@ impl MangoClient {
|
|||
owner: self.owner(),
|
||||
bank: mint_info.first_bank(),
|
||||
vault: mint_info.first_vault(),
|
||||
oracle: mint_info.oracle,
|
||||
token_account: get_associated_token_address(
|
||||
&self.owner(),
|
||||
&mint_info.mint,
|
||||
|
@ -603,6 +605,10 @@ impl MangoClient {
|
|||
let rates = get_fee_rates(fee_tier);
|
||||
(s3.market.pc_lot_size as f64 * (1f64 + rates.0)) as u64 * (limit_price * max_base_qty)
|
||||
};
|
||||
let payer_mint_info = match side {
|
||||
Serum3Side::Bid => s3.quote.mint_info,
|
||||
Serum3Side::Ask => s3.base.mint_info,
|
||||
};
|
||||
|
||||
self.program()
|
||||
.request()
|
||||
|
@ -614,10 +620,8 @@ impl MangoClient {
|
|||
group: self.group(),
|
||||
account: self.mango_account_address,
|
||||
open_orders,
|
||||
quote_bank: s3.quote.mint_info.first_bank(),
|
||||
quote_vault: s3.quote.mint_info.first_vault(),
|
||||
base_bank: s3.base.mint_info.first_bank(),
|
||||
base_vault: s3.base.mint_info.first_vault(),
|
||||
payer_bank: payer_mint_info.first_bank(),
|
||||
payer_vault: payer_mint_info.first_vault(),
|
||||
serum_market: s3.market.address,
|
||||
serum_program: s3.market.market.serum_program,
|
||||
serum_market_external: s3.market.market.serum_market_external,
|
||||
|
|
|
@ -217,10 +217,9 @@ impl MangoGroupContext {
|
|||
oracles.push(mint_info.oracle);
|
||||
}
|
||||
for affected_token_index in affected_tokens {
|
||||
if account
|
||||
if !account
|
||||
.active_token_positions()
|
||||
.find(|p| p.token_index == affected_token_index)
|
||||
.is_none()
|
||||
.any(|p| p.token_index == affected_token_index)
|
||||
{
|
||||
// If there is not yet an active position for the token, we need to pass
|
||||
// the bank/oracle for health check anyway.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Dockerfile for keeper service in Heroku
|
||||
# heroku container:push keeper -R -a HEROKU_APP_NAME
|
||||
# heroku container:release -a HEROKU_APP_NAME
|
||||
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:latest
|
||||
ARG BASE_TAG=latest
|
||||
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:$BASE_TAG
|
||||
ENTRYPOINT ["keeper"]
|
||||
CMD ["crank"]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{sync::Arc, time::Duration};
|
||||
use std::{sync::Arc, time::Duration, time::Instant};
|
||||
|
||||
use crate::MangoClient;
|
||||
use itertools::Itertools;
|
||||
|
@ -76,7 +76,7 @@ pub async fn loop_update_index_and_rate(
|
|||
let token_names = token_indices_clone
|
||||
.iter()
|
||||
.map(|token_index| client.context.token(*token_index).name.to_owned())
|
||||
.join(", ");
|
||||
.join(",");
|
||||
|
||||
let program = client.program();
|
||||
let mut req = program.request();
|
||||
|
@ -112,16 +112,24 @@ pub async fn loop_update_index_and_rate(
|
|||
ix.accounts.append(&mut banks);
|
||||
req = req.instruction(ix);
|
||||
}
|
||||
let pre = Instant::now();
|
||||
let sig_result = req.send().map_err(prettify_client_error);
|
||||
|
||||
if let Err(e) = sig_result {
|
||||
log::info!(
|
||||
"metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}",
|
||||
token_names,
|
||||
pre.elapsed().as_millis(),
|
||||
e
|
||||
);
|
||||
log::error!("{:?}", e)
|
||||
} else {
|
||||
log::info!(
|
||||
"update_index_and_rate {} {:?}",
|
||||
"metricName=UpdateTokensV4Success tokens={} durationMs={}",
|
||||
token_names,
|
||||
sig_result.unwrap()
|
||||
)
|
||||
pre.elapsed().as_millis(),
|
||||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -191,6 +199,7 @@ pub async fn loop_consume_events(
|
|||
event_queue.pop_front()?;
|
||||
}
|
||||
|
||||
let pre = Instant::now();
|
||||
let sig_result = client
|
||||
.program()
|
||||
.request()
|
||||
|
@ -216,13 +225,22 @@ pub async fn loop_consume_events(
|
|||
.map_err(prettify_client_error);
|
||||
|
||||
if let Err(e) = sig_result {
|
||||
log::info!(
|
||||
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
|
||||
perp_market.name(),
|
||||
pre.elapsed().as_millis(),
|
||||
ams_.len(),
|
||||
e.to_string()
|
||||
);
|
||||
log::error!("{:?}", e)
|
||||
} else {
|
||||
log::info!(
|
||||
"consume_event {} {:?}",
|
||||
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
|
||||
perp_market.name(),
|
||||
sig_result.unwrap()
|
||||
)
|
||||
pre.elapsed().as_millis(),
|
||||
ams_.len(),
|
||||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -253,6 +271,7 @@ pub async fn loop_update_funding(
|
|||
|
||||
let client = mango_client.clone();
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
let pre = Instant::now();
|
||||
let sig_result = client
|
||||
.program()
|
||||
.request()
|
||||
|
@ -275,13 +294,20 @@ pub async fn loop_update_funding(
|
|||
.send()
|
||||
.map_err(prettify_client_error);
|
||||
if let Err(e) = sig_result {
|
||||
log::error!(
|
||||
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
|
||||
perp_market.name(),
|
||||
pre.elapsed().as_millis(),
|
||||
e.to_string()
|
||||
);
|
||||
log::error!("{:?}", e)
|
||||
} else {
|
||||
log::info!(
|
||||
"update_funding {} {:?}",
|
||||
"metricName=UpdateFundingV4Success market={} durationMs={}",
|
||||
perp_market.name(),
|
||||
sig_result.unwrap()
|
||||
)
|
||||
pre.elapsed().as_millis(),
|
||||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -27,7 +27,7 @@ pub async fn runner(
|
|||
market_name
|
||||
.split('/')
|
||||
.collect::<Vec<&str>>()
|
||||
.get(0)
|
||||
.first()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -77,7 +77,7 @@ fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
|
|||
let account = mango_client.mango_account()?;
|
||||
|
||||
for (market_index, serum3_market) in mango_client.context.serum3_markets.iter() {
|
||||
if account.serum3_orders(*market_index).is_none() {
|
||||
if account.serum3_orders(*market_index).is_err() {
|
||||
mango_client.serum3_create_open_orders(serum3_market.market.name())?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Dockerfile for keeper service in Heroku
|
||||
# heroku container:push keeper -R -a HEROKU_APP_NAME
|
||||
# heroku container:release -a HEROKU_APP_NAME
|
||||
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:latest
|
||||
ARG BASE_TAG=latest
|
||||
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:$BASE_TAG
|
||||
CMD ["liquidator"]
|
||||
|
|
|
@ -4,8 +4,8 @@ use crate::account_shared_data::KeyedAccountSharedData;
|
|||
|
||||
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
|
||||
use mango_v4::state::{
|
||||
new_health_cache, oracle_price, Bank, FixedOrderAccountRetriever, HealthCache, HealthType,
|
||||
MangoAccountValue, TokenIndex, QUOTE_TOKEN_INDEX,
|
||||
new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue,
|
||||
TokenIndex, QUOTE_TOKEN_INDEX,
|
||||
};
|
||||
|
||||
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||
|
@ -143,11 +143,10 @@ pub fn maybe_liquidate_account(
|
|||
let token = mango_client.context.token(token_position.token_index);
|
||||
let bank = account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())?;
|
||||
let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?;
|
||||
let price = oracle_price(
|
||||
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||
bank.oracle_config.conf_filter,
|
||||
bank.mint_decimals,
|
||||
)?;
|
||||
let price = bank.oracle_price(&KeyedAccountSharedData::new(
|
||||
token.mint_info.oracle,
|
||||
oracle.into(),
|
||||
))?;
|
||||
Ok((
|
||||
token_position.token_index,
|
||||
price,
|
||||
|
|
|
@ -332,13 +332,13 @@ fn liquidate<'a>(
|
|||
config: &liquidate::Config,
|
||||
rebalance_config: &rebalance::Config,
|
||||
) -> anyhow::Result<()> {
|
||||
if !liquidate::maybe_liquidate_one(&mango_client, &account_fetcher, accounts, &config) {
|
||||
if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let liqor = &mango_client.mango_account_address;
|
||||
if let Err(err) =
|
||||
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, &rebalance_config)
|
||||
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config)
|
||||
{
|
||||
log::error!("failed to rebalance liqor: {:?}", err);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{account_shared_data::KeyedAccountSharedData, AnyhowWrap};
|
||||
|
||||
use client::{chain_data, AccountFetcher, MangoClient, TokenContext};
|
||||
use mango_v4::state::{oracle_price, Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX};
|
||||
use mango_v4::state::{Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX};
|
||||
|
||||
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
|
@ -44,11 +44,10 @@ impl TokenState {
|
|||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<I80F48> {
|
||||
let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?;
|
||||
oracle_price(
|
||||
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||
bank.oracle_config.conf_filter,
|
||||
bank.mint_decimals,
|
||||
)
|
||||
bank.oracle_price(&KeyedAccountSharedData::new(
|
||||
token.mint_info.oracle,
|
||||
oracle.into(),
|
||||
))
|
||||
.map_err_anyhow()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,3 +58,4 @@ async-trait = "0.1.52"
|
|||
margin-trade = { path = "../margin-trade", features = ["cpi"] }
|
||||
itertools = "0.10.3"
|
||||
rand = "0.8.4"
|
||||
lazy_static = "1.4.0"
|
||||
|
|
|
@ -19,6 +19,8 @@ pub enum MangoError {
|
|||
InvalidFlashLoanTargetCpiProgram,
|
||||
#[msg("health must be positive")]
|
||||
HealthMustBePositive,
|
||||
#[msg("health must be positive or increase")]
|
||||
HealthMustBePositiveOrIncrease,
|
||||
#[msg("health must be negative")]
|
||||
HealthMustBeNegative,
|
||||
#[msg("the account is bankrupt")]
|
||||
|
@ -35,6 +37,8 @@ pub enum MangoError {
|
|||
Serum3OpenOrdersExistAlready,
|
||||
#[msg("bank vault has insufficent funds")]
|
||||
InsufficentBankVaultFunds,
|
||||
#[msg("account is currently being liquidated")]
|
||||
BeingLiquidated,
|
||||
}
|
||||
|
||||
pub trait Contextable {
|
||||
|
|
|
@ -4,8 +4,8 @@ use crate::group_seeds;
|
|||
use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog};
|
||||
use crate::state::MangoAccount;
|
||||
use crate::state::{
|
||||
compute_health, compute_health_from_fixed_accounts, new_fixed_order_account_retriever,
|
||||
AccountLoaderDynamic, AccountRetriever, Bank, Group, HealthType, TokenIndex,
|
||||
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, AccountRetriever,
|
||||
Bank, Group, TokenIndex,
|
||||
};
|
||||
use crate::util::checked_math as cm;
|
||||
use anchor_lang::prelude::*;
|
||||
|
@ -72,7 +72,7 @@ pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>(
|
|||
let token_accounts = &ctx.remaining_accounts[2 * num_loans..3 * num_loans];
|
||||
let group_ai = &ctx.remaining_accounts[3 * num_loans];
|
||||
|
||||
let group_al = AccountLoader::<Group>::try_from(&group_ai)?;
|
||||
let group_al = AccountLoader::<Group>::try_from(group_ai)?;
|
||||
let group = group_al.load()?;
|
||||
let group_seeds = group_seeds!(group);
|
||||
let seeds = [&group_seeds[..]];
|
||||
|
@ -316,7 +316,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
|
||||
// Check health before balance adjustments
|
||||
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
||||
let _pre_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
|
||||
let health_cache = new_health_cache(&account.borrow(), &retriever)?;
|
||||
let pre_health = account.check_health_pre(&health_cache)?;
|
||||
|
||||
// Prices for logging
|
||||
let mut prices = vec![];
|
||||
|
@ -388,13 +389,9 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
});
|
||||
|
||||
// Check health after account position changes
|
||||
let post_health =
|
||||
compute_health_from_fixed_accounts(&account.borrow(), HealthType::Init, health_ais)?;
|
||||
msg!("post_cpi_health {:?}", post_health);
|
||||
require!(post_health >= 0, MangoError::HealthMustBePositive);
|
||||
account
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(post_health);
|
||||
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
||||
let health_cache = new_health_cache(&account.borrow(), &retriever)?;
|
||||
account.check_health_post(&health_cache, pre_health)?;
|
||||
|
||||
// Deactivate inactive token accounts after health check
|
||||
for raw_token_index in deactivated_token_positions {
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
|
||||
use anchor_lang::Discriminator;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
/// Sets up for a health region
|
||||
///
|
||||
/// The same transaction must have the corresponding HealthRegionEnd call.
|
||||
///
|
||||
/// remaining_accounts: health accounts for account
|
||||
#[derive(Accounts)]
|
||||
pub struct HealthRegionBegin<'info> {
|
||||
/// Instructions Sysvar for instruction introspection
|
||||
/// CHECK: fixed instructions sysvar account
|
||||
#[account(address = tx_instructions::ID)]
|
||||
pub instructions: UncheckedAccount<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
pub account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||
}
|
||||
|
||||
/// Ends a health region.
|
||||
///
|
||||
/// remaining_accounts: health accounts for account
|
||||
#[derive(Accounts)]
|
||||
pub struct HealthRegionEnd<'info> {
|
||||
#[account(mut)]
|
||||
pub account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||
}
|
||||
|
||||
pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>(
|
||||
ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionBegin<'info>>,
|
||||
) -> Result<()> {
|
||||
// Check if the other instructions in the transactions are compatible
|
||||
{
|
||||
let ixs = ctx.accounts.instructions.as_ref();
|
||||
let current_index = tx_instructions::load_current_index_checked(ixs)? as usize;
|
||||
|
||||
// There must be a matching HealthRegionEnd instruction
|
||||
let mut index = current_index + 1;
|
||||
let mut found_end = false;
|
||||
loop {
|
||||
let ix = match tx_instructions::load_instruction_at_checked(index, ixs) {
|
||||
Ok(ix) => ix,
|
||||
Err(ProgramError::InvalidArgument) => break, // past the last instruction
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
index += 1;
|
||||
|
||||
if ix.program_id != crate::id() {
|
||||
continue;
|
||||
}
|
||||
if ix.data[0..8] != crate::instruction::HealthRegionEnd::discriminator() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check that it's for the same account
|
||||
require_keys_eq!(ix.accounts[0].pubkey, ctx.accounts.account.key());
|
||||
|
||||
found_end = true;
|
||||
index += 1;
|
||||
}
|
||||
require_msg!(
|
||||
found_end,
|
||||
"found no HealthRegionEnd instruction in transaction"
|
||||
);
|
||||
}
|
||||
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require_msg!(
|
||||
!account.fixed.is_in_health_region(),
|
||||
"account must not already be health wrapped"
|
||||
);
|
||||
account.fixed.set_in_health_region(true);
|
||||
|
||||
let group = account.fixed.group;
|
||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group)
|
||||
.context("create account retriever")?;
|
||||
|
||||
// Compute pre-health and store it on the account
|
||||
let health_cache = new_health_cache(&account.borrow(), &account_retriever)?;
|
||||
let pre_health = account.check_health_pre(&health_cache)?;
|
||||
account.fixed.health_region_begin_init_health = pre_health.ceil().checked_to_num().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn health_region_end<'key, 'accounts, 'remaining, 'info>(
|
||||
ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionEnd<'info>>,
|
||||
) -> Result<()> {
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require_msg!(
|
||||
account.fixed.is_in_health_region(),
|
||||
"account must be health wrapped"
|
||||
);
|
||||
account.fixed.set_in_health_region(false);
|
||||
|
||||
let group = account.fixed.group;
|
||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group)
|
||||
.context("create account retriever")?;
|
||||
let health_cache = new_health_cache(&account.borrow(), &account_retriever)?;
|
||||
|
||||
let pre_health = I80F48::from(account.fixed.health_region_begin_init_health);
|
||||
account.check_health_post(&health_cache, pre_health)?;
|
||||
account.fixed.health_region_begin_init_health = 0;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -89,6 +89,7 @@ pub fn liq_token_bankruptcy(
|
|||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
|
||||
|
||||
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
||||
|
||||
|
@ -106,7 +107,7 @@ pub fn liq_token_bankruptcy(
|
|||
let mut liab_deposit_index = liab_bank.deposit_index;
|
||||
let liab_borrow_index = liab_bank.borrow_index;
|
||||
let (liqee_liab, liqee_raw_token_index) = liqee.token_position_mut(liab_token_index)?;
|
||||
let initial_liab_native = liqee_liab.native(&liab_bank);
|
||||
let initial_liab_native = liqee_liab.native(liab_bank);
|
||||
let mut remaining_liab_loss = -initial_liab_native;
|
||||
require_gt!(remaining_liab_loss, I80F48::ZERO);
|
||||
|
||||
|
@ -178,9 +179,11 @@ pub fn liq_token_bankruptcy(
|
|||
liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?;
|
||||
|
||||
// Check liqor's health
|
||||
let liqor_health =
|
||||
compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
if !liqor.fixed.is_in_health_region() {
|
||||
let liqor_health =
|
||||
compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
}
|
||||
|
||||
// liqor quote
|
||||
emit!(TokenBalanceLog {
|
||||
|
@ -304,7 +307,7 @@ pub fn liq_token_bankruptcy(
|
|||
mango_group: ctx.accounts.group.key(),
|
||||
liqee: ctx.accounts.liqee.key(),
|
||||
liqor: ctx.accounts.liqor.key(),
|
||||
liab_token_index: liab_token_index,
|
||||
liab_token_index,
|
||||
initial_liab_native: initial_liab_native.to_bits(),
|
||||
liab_price: liab_price.to_bits(),
|
||||
insurance_token_index: QUOTE_TOKEN_INDEX,
|
||||
|
|
|
@ -50,6 +50,7 @@ pub fn liq_token_with_token(
|
|||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
|
||||
|
||||
let mut liqee = ctx.accounts.liqee.load_mut()?;
|
||||
|
||||
|
@ -62,8 +63,7 @@ pub fn liq_token_with_token(
|
|||
// we want to allow liquidation to continue until init_health is positive,
|
||||
// to prevent constant oscillation between the two states
|
||||
if liqee.being_liquidated() {
|
||||
if init_health > I80F48::ZERO {
|
||||
liqee.fixed.set_being_liquidated(false);
|
||||
if liqee.fixed.maybe_recover_from_being_liquidated(init_health) {
|
||||
msg!("Liqee init_health above zero");
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ pub fn liq_token_with_token(
|
|||
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 liqee_liab_native_after = liqee_liab_position.native(&liab_bank);
|
||||
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)?;
|
||||
|
@ -161,7 +161,7 @@ pub fn liq_token_with_token(
|
|||
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_assets_native_after = liqee_asset_position.native(&asset_bank);
|
||||
let liqee_assets_native_after = liqee_asset_position.native(asset_bank);
|
||||
|
||||
// Update the health cache
|
||||
liqee_health_cache.adjust_token_balance(
|
||||
|
@ -251,9 +251,11 @@ pub fn liq_token_with_token(
|
|||
.maybe_recover_from_being_liquidated(liqee_init_health);
|
||||
|
||||
// Check liqor's health
|
||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
||||
.context("compute liqor health")?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
if !liqor.fixed.is_in_health_region() {
|
||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
||||
.context("compute liqor health")?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
}
|
||||
|
||||
emit!(LiquidateTokenAndTokenLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
|
|
|
@ -8,6 +8,7 @@ pub use flash_loan::*;
|
|||
pub use group_close::*;
|
||||
pub use group_create::*;
|
||||
pub use group_edit::*;
|
||||
pub use health_region::*;
|
||||
pub use liq_token_bankruptcy::*;
|
||||
pub use liq_token_with_token::*;
|
||||
pub use perp_cancel_all_orders::*;
|
||||
|
@ -51,6 +52,7 @@ mod flash_loan;
|
|||
mod group_close;
|
||||
mod group_create;
|
||||
mod group_edit;
|
||||
mod health_region;
|
||||
mod liq_token_bankruptcy;
|
||||
mod liq_token_with_token;
|
||||
mod perp_cancel_all_orders;
|
||||
|
|
|
@ -4,8 +4,8 @@ use crate::accounts_zerocopy::*;
|
|||
use crate::error::*;
|
||||
use crate::state::MangoAccount;
|
||||
use crate::state::{
|
||||
compute_health, new_fixed_order_account_retriever, oracle_price, AccountLoaderDynamic, Book,
|
||||
BookSide, EventQueue, Group, HealthType, OrderType, PerpMarket, Side,
|
||||
new_fixed_order_account_retriever, new_health_cache, oracle_price, AccountLoaderDynamic, Book,
|
||||
BookSide, EventQueue, Group, OrderType, PerpMarket, Side,
|
||||
};
|
||||
|
||||
#[derive(Accounts)]
|
||||
|
@ -83,60 +83,81 @@ pub fn perp_place_order(
|
|||
|
||||
let account_pk = ctx.accounts.account.key();
|
||||
|
||||
{
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
let mut book = Book::new(bids, asks);
|
||||
let perp_market_index = {
|
||||
let perp_market = ctx.accounts.perp_market.load()?;
|
||||
perp_market.perp_market_index
|
||||
};
|
||||
let (_, perp_position_raw_index) = account.ensure_perp_position(perp_market_index)?;
|
||||
|
||||
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
|
||||
//
|
||||
// Pre-health computation, _after_ perp position is created
|
||||
//
|
||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
||||
let pre_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_health))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let oracle_price = oracle_price(
|
||||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||
perp_market.oracle_config.conf_filter,
|
||||
perp_market.base_token_decimals,
|
||||
)?;
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
let mut book = Book::new(bids, asks);
|
||||
|
||||
let now_ts = Clock::get()?.unix_timestamp as u64;
|
||||
let time_in_force = if expiry_timestamp != 0 {
|
||||
// If expiry is far in the future, clamp to 255 seconds
|
||||
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
|
||||
if tif == 0 {
|
||||
// If expiry is in the past, ignore the order
|
||||
msg!("Order is already expired");
|
||||
return Ok(());
|
||||
}
|
||||
tif as u8
|
||||
} else {
|
||||
// Never expire
|
||||
0
|
||||
};
|
||||
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
|
||||
|
||||
// TODO reduce_only based on event queue
|
||||
let oracle_price = oracle_price(
|
||||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||
perp_market.oracle_config.conf_filter,
|
||||
perp_market.base_token_decimals,
|
||||
)?;
|
||||
|
||||
book.new_order(
|
||||
side,
|
||||
&mut perp_market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&account_pk,
|
||||
price_lots,
|
||||
max_base_lots,
|
||||
max_quote_lots,
|
||||
order_type,
|
||||
time_in_force,
|
||||
client_order_id,
|
||||
now_ts,
|
||||
limit,
|
||||
)?;
|
||||
let now_ts = Clock::get()?.unix_timestamp as u64;
|
||||
let time_in_force = if expiry_timestamp != 0 {
|
||||
// If expiry is far in the future, clamp to 255 seconds
|
||||
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
|
||||
if tif == 0 {
|
||||
// If expiry is in the past, ignore the order
|
||||
msg!("Order is already expired");
|
||||
return Ok(());
|
||||
}
|
||||
tif as u8
|
||||
} else {
|
||||
// Never expire
|
||||
0
|
||||
};
|
||||
|
||||
// TODO reduce_only based on event queue
|
||||
|
||||
book.new_order(
|
||||
side,
|
||||
&mut perp_market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&account_pk,
|
||||
price_lots,
|
||||
max_base_lots,
|
||||
max_quote_lots,
|
||||
order_type,
|
||||
time_in_force,
|
||||
client_order_id,
|
||||
now_ts,
|
||||
limit,
|
||||
)?;
|
||||
|
||||
//
|
||||
// Health check
|
||||
//
|
||||
if let Some((mut health_cache, pre_health)) = pre_health_opt {
|
||||
let perp_position = account.perp_position_by_raw_index(perp_position_raw_index);
|
||||
health_cache.recompute_perp_info(perp_position, &perp_market)?;
|
||||
account.check_health_post(&health_cache, pre_health)?;
|
||||
}
|
||||
|
||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
|
||||
msg!("health: {}", health);
|
||||
require!(health >= 0, MangoError::HealthMustBePositive);
|
||||
account.fixed.maybe_recover_from_being_liquidated(health);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -7,12 +7,15 @@ use crate::state::*;
|
|||
pub struct Serum3CancelAllOrders<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(has_one = group)]
|
||||
#[account(
|
||||
has_one = group
|
||||
// owner is checked at #1
|
||||
)]
|
||||
pub account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
|
||||
pub open_orders: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
|
@ -46,6 +49,7 @@ pub fn serum3_cancel_all_orders(ctx: Context<Serum3CancelAllOrders>, limit: u8)
|
|||
//
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
// account constraint #1
|
||||
require!(
|
||||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
||||
MangoError::SomeError
|
||||
|
@ -53,11 +57,10 @@ pub fn serum3_cancel_all_orders(ctx: Context<Serum3CancelAllOrders>, limit: u8)
|
|||
|
||||
let serum_market = ctx.accounts.serum_market.load()?;
|
||||
|
||||
// Validate open_orders
|
||||
// Validate open_orders #2
|
||||
require!(
|
||||
account
|
||||
.serum3_orders(serum_market.market_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?
|
||||
.serum3_orders(serum_market.market_index)?
|
||||
.open_orders
|
||||
== ctx.accounts.open_orders.key(),
|
||||
MangoError::SomeError
|
||||
|
|
|
@ -3,23 +3,24 @@ use anchor_lang::prelude::*;
|
|||
use serum_dex::instruction::CancelOrderInstructionV2;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::serum3_cpi::load_open_orders_ref;
|
||||
use crate::state::*;
|
||||
|
||||
use super::OpenOrdersSlim;
|
||||
use super::Serum3Side;
|
||||
use checked_math as cm;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Serum3CancelOrder<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(mut, has_one = group)]
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group
|
||||
// owner is checked at #1
|
||||
)]
|
||||
pub account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
|
||||
pub open_orders: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
|
@ -59,16 +60,16 @@ pub fn serum3_cancel_order(
|
|||
//
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
// account constraint #1
|
||||
require!(
|
||||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
// Validate open_orders
|
||||
// Validate open_orders #2
|
||||
require!(
|
||||
account
|
||||
.serum3_orders(serum_market.market_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?
|
||||
.serum3_orders(serum_market.market_index)?
|
||||
.open_orders
|
||||
== ctx.accounts.open_orders.key(),
|
||||
MangoError::SomeError
|
||||
|
@ -78,55 +79,15 @@ pub fn serum3_cancel_order(
|
|||
//
|
||||
// Cancel
|
||||
//
|
||||
let before_oo = {
|
||||
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
|
||||
OpenOrdersSlim::from_oo(&open_orders)
|
||||
};
|
||||
let order = serum_dex::instruction::CancelOrderInstructionV2 {
|
||||
side: u8::try_from(side).unwrap().try_into().unwrap(),
|
||||
order_id,
|
||||
};
|
||||
cpi_cancel_order(ctx.accounts, order)?;
|
||||
|
||||
{
|
||||
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
|
||||
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
decrease_maybe_loan(
|
||||
serum_market.market_index,
|
||||
&mut account.borrow_mut(),
|
||||
&before_oo,
|
||||
&after_oo,
|
||||
);
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// if free has increased, the free increase is reduction in reserved, reduce this from
|
||||
// the cached
|
||||
pub fn decrease_maybe_loan(
|
||||
market_index: Serum3MarketIndex,
|
||||
account: &mut MangoAccountRefMut,
|
||||
before_oo: &OpenOrdersSlim,
|
||||
after_oo: &OpenOrdersSlim,
|
||||
) {
|
||||
let serum3_account = account.serum3_orders_mut(market_index).unwrap();
|
||||
|
||||
if after_oo.native_coin_free > before_oo.native_coin_free {
|
||||
let native_coin_free_increase = after_oo.native_coin_free - before_oo.native_coin_free;
|
||||
serum3_account.previous_native_coin_reserved =
|
||||
cm!(serum3_account.previous_native_coin_reserved - native_coin_free_increase);
|
||||
}
|
||||
|
||||
// pc
|
||||
if after_oo.native_pc_free > before_oo.native_pc_free {
|
||||
let free_pc_increase = after_oo.native_pc_free - before_oo.native_pc_free;
|
||||
serum3_account.previous_native_pc_reserved =
|
||||
cm!(serum3_account.previous_native_pc_reserved - free_pc_increase);
|
||||
}
|
||||
}
|
||||
|
||||
fn cpi_cancel_order(ctx: &Serum3CancelOrder, order: CancelOrderInstructionV2) -> Result<()> {
|
||||
use crate::serum3_cpi;
|
||||
let group = ctx.group.load()?;
|
||||
|
|
|
@ -7,7 +7,11 @@ use crate::state::*;
|
|||
pub struct Serum3CloseOpenOrders<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(mut, has_one = group)]
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group
|
||||
// owner is checked at #1
|
||||
)]
|
||||
pub account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
|
@ -23,7 +27,7 @@ pub struct Serum3CloseOpenOrders<'info> {
|
|||
pub serum_market_external: UncheckedAccount<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
|
||||
pub open_orders: UncheckedAccount<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
|
@ -36,6 +40,7 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
|
|||
// Validation
|
||||
//
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
// account constraint #1
|
||||
require!(
|
||||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
||||
MangoError::SomeError
|
||||
|
@ -43,11 +48,10 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
|
|||
|
||||
let serum_market = ctx.accounts.serum_market.load()?;
|
||||
|
||||
// Validate open_orders
|
||||
// Validate open_orders #2
|
||||
require!(
|
||||
account
|
||||
.serum3_orders(serum_market.market_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?
|
||||
.serum3_orders(serum_market.market_index)?
|
||||
.open_orders
|
||||
== ctx.accounts.open_orders.key(),
|
||||
MangoError::SomeError
|
||||
|
@ -58,7 +62,14 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
|
|||
//
|
||||
cpi_close_open_orders(ctx.accounts)?;
|
||||
|
||||
// TODO: decrement in_use_count on the base token and quote token
|
||||
// Reduce the in_use_count on the token positions - they no longer need to be forced open.
|
||||
// We cannot immediately dust tiny positions because we don't have the banks.
|
||||
let (base_position, _) = account.token_position_mut(serum_market.base_token_index)?;
|
||||
base_position.in_use_count = base_position.in_use_count.saturating_sub(1);
|
||||
let (quote_position, _) = account.token_position_mut(serum_market.quote_token_index)?;
|
||||
quote_position.in_use_count = quote_position.in_use_count.saturating_sub(1);
|
||||
|
||||
// Deactivate the serum open orders account itself
|
||||
account.deactivate_serum3_orders(serum_market.market_index)?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -7,7 +7,11 @@ use crate::state::*;
|
|||
pub struct Serum3CreateOpenOrders<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(mut, has_one = group)]
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group
|
||||
// owner is checked at #1
|
||||
)]
|
||||
pub account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
|
@ -48,6 +52,7 @@ pub fn serum3_create_open_orders(ctx: Context<Serum3CreateOpenOrders>) -> Result
|
|||
let serum_market = ctx.accounts.serum_market.load()?;
|
||||
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
// account constraint #1
|
||||
require!(
|
||||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
||||
MangoError::SomeError
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{Token, TokenAccount};
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::instructions::apply_vault_difference;
|
||||
use crate::instructions::{
|
||||
apply_vault_difference, charge_loan_origination_fees, OODifference, OpenOrdersSlim,
|
||||
};
|
||||
use crate::serum3_cpi::load_open_orders_ref;
|
||||
use crate::state::*;
|
||||
|
||||
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Serum3LiqForceCancelOrders<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
@ -15,7 +17,7 @@ pub struct Serum3LiqForceCancelOrders<'info> {
|
|||
pub account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||
|
||||
#[account(mut)]
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
|
||||
pub open_orders: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
|
@ -50,7 +52,7 @@ pub struct Serum3LiqForceCancelOrders<'info> {
|
|||
/// CHECK: Validated by the serum cpi call
|
||||
pub market_vault_signer: UncheckedAccount<'info>,
|
||||
|
||||
// token_index and bank.vault == vault is validated inline
|
||||
// token_index and bank.vault == vault is validated inline at #3
|
||||
#[account(mut, has_one = group)]
|
||||
pub quote_bank: AccountLoader<'info, Bank>,
|
||||
#[account(mut)]
|
||||
|
@ -74,17 +76,16 @@ pub fn serum3_liq_force_cancel_orders(
|
|||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
|
||||
// Validate open_orders
|
||||
// Validate open_orders #2
|
||||
require!(
|
||||
account
|
||||
.serum3_orders(serum_market.market_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?
|
||||
.serum3_orders(serum_market.market_index)?
|
||||
.open_orders
|
||||
== ctx.accounts.open_orders.key(),
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
// Validate banks and vaults
|
||||
// Validate banks and vaults #3
|
||||
let quote_bank = ctx.accounts.quote_bank.load()?;
|
||||
require!(
|
||||
quote_bank.vault == ctx.accounts.quote_vault.key(),
|
||||
|
@ -105,16 +106,58 @@ pub fn serum3_liq_force_cancel_orders(
|
|||
);
|
||||
}
|
||||
|
||||
// TODO: do the correct health / being_liquidated check
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
|
||||
//
|
||||
// Check liqee health if liquidation is allowed
|
||||
//
|
||||
let mut health_cache = {
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health = compute_health(&account.borrow(), HealthType::Maint, &retriever)?;
|
||||
msg!("health: {}", health);
|
||||
require!(health < 0, MangoError::SomeError);
|
||||
}
|
||||
let health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("create health cache")?;
|
||||
|
||||
if account.being_liquidated() {
|
||||
let init_health = health_cache.health(HealthType::Init);
|
||||
if account
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(init_health)
|
||||
{
|
||||
msg!("Liqee init_health above zero");
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
require!(
|
||||
maint_health < I80F48::ZERO,
|
||||
MangoError::HealthMustBeNegative
|
||||
);
|
||||
account.fixed.set_being_liquidated(true);
|
||||
}
|
||||
|
||||
health_cache
|
||||
};
|
||||
|
||||
//
|
||||
// Charge any open loan origination fees
|
||||
//
|
||||
let before_oo = {
|
||||
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
|
||||
let before_oo = OpenOrdersSlim::from_oo(&open_orders);
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
|
||||
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
charge_loan_origination_fees(
|
||||
&ctx.accounts.group.key(),
|
||||
&ctx.accounts.account.key(),
|
||||
serum_market.market_index,
|
||||
&mut base_bank,
|
||||
&mut quote_bank,
|
||||
&mut account.borrow_mut(),
|
||||
&before_oo,
|
||||
)?;
|
||||
|
||||
before_oo
|
||||
};
|
||||
|
||||
//
|
||||
// Before-settle tracking
|
||||
|
@ -131,45 +174,51 @@ pub fn serum3_liq_force_cancel_orders(
|
|||
//
|
||||
// After-settle tracking
|
||||
//
|
||||
{
|
||||
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);
|
||||
OODifference::new(&before_oo, &after_oo)
|
||||
.adjust_health_cache(&mut health_cache, &serum_market)?;
|
||||
};
|
||||
|
||||
ctx.accounts.base_vault.reload()?;
|
||||
ctx.accounts.quote_vault.reload()?;
|
||||
let after_base_vault = ctx.accounts.base_vault.amount;
|
||||
let after_quote_vault = ctx.accounts.quote_vault.amount;
|
||||
|
||||
// Charge the difference in vault balances to the user's account
|
||||
// Settle cannot decrease vault balances
|
||||
require_gte!(after_base_vault, before_base_vault);
|
||||
require_gte!(after_quote_vault, before_quote_vault);
|
||||
|
||||
// Credit the difference in vault balances to the user's account
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
|
||||
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) =
|
||||
apply_vault_difference(
|
||||
&mut account.borrow_mut(),
|
||||
&mut base_bank,
|
||||
after_base_vault,
|
||||
before_base_vault,
|
||||
&mut quote_bank,
|
||||
after_quote_vault,
|
||||
before_quote_vault,
|
||||
)?;
|
||||
vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut());
|
||||
apply_vault_difference(
|
||||
&mut account.borrow_mut(),
|
||||
serum_market.market_index,
|
||||
&mut base_bank,
|
||||
after_base_vault,
|
||||
before_base_vault,
|
||||
)?
|
||||
.adjust_health_cache(&mut health_cache)?;
|
||||
apply_vault_difference(
|
||||
&mut account.borrow_mut(),
|
||||
serum_market.market_index,
|
||||
&mut quote_bank,
|
||||
after_quote_vault,
|
||||
before_quote_vault,
|
||||
)?
|
||||
.adjust_health_cache(&mut health_cache)?;
|
||||
|
||||
if base_loan_origination_fee.is_positive() {
|
||||
emit!(WithdrawLoanOriginationFeeLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: serum_market.base_token_index,
|
||||
loan_origination_fee: base_loan_origination_fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders
|
||||
});
|
||||
}
|
||||
if quote_loan_origination_fee.is_positive() {
|
||||
emit!(WithdrawLoanOriginationFeeLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: serum_market.quote_token_index,
|
||||
loan_origination_fee: quote_loan_origination_fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders
|
||||
});
|
||||
}
|
||||
//
|
||||
// Health check at the end
|
||||
//
|
||||
let init_health = health_cache.health(HealthType::Init);
|
||||
account
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(init_health);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::error::*;
|
||||
|
||||
use crate::serum3_cpi::load_open_orders_ref;
|
||||
use crate::serum3_cpi::{load_market_state, load_open_orders_ref};
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{Token, TokenAccount};
|
||||
|
@ -9,17 +9,16 @@ use fixed::types::I80F48;
|
|||
use num_enum::IntoPrimitive;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use serum_dex::instruction::NewOrderInstructionV3;
|
||||
use serum_dex::matching::Side;
|
||||
use serum_dex::state::OpenOrders;
|
||||
|
||||
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
|
||||
|
||||
/// For loan origination fees bookkeeping purposes
|
||||
#[derive(Debug)]
|
||||
pub struct OpenOrdersSlim {
|
||||
pub native_coin_free: u64,
|
||||
pub native_coin_total: u64,
|
||||
pub native_pc_free: u64,
|
||||
pub native_pc_total: u64,
|
||||
native_coin_free: u64,
|
||||
native_coin_total: u64,
|
||||
native_pc_free: u64,
|
||||
native_pc_total: u64,
|
||||
referrer_rebates_accrued: u64,
|
||||
}
|
||||
impl OpenOrdersSlim {
|
||||
pub fn from_oo(oo: &OpenOrders) -> Self {
|
||||
|
@ -28,30 +27,66 @@ impl OpenOrdersSlim {
|
|||
native_coin_total: oo.native_coin_total,
|
||||
native_pc_free: oo.native_pc_free,
|
||||
native_pc_total: oo.native_pc_total,
|
||||
referrer_rebates_accrued: oo.referrer_rebates_accrued,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OpenOrdersReserved {
|
||||
fn native_coin_reserved(&self) -> u64;
|
||||
fn native_pc_reserved(&self) -> u64;
|
||||
pub trait OpenOrdersAmounts {
|
||||
fn native_base_reserved(&self) -> u64;
|
||||
fn native_quote_reserved(&self) -> u64;
|
||||
fn native_base_free(&self) -> u64;
|
||||
fn native_quote_free(&self) -> u64;
|
||||
fn native_quote_free_plus_rebates(&self) -> u64;
|
||||
fn native_base_total(&self) -> u64;
|
||||
fn native_quote_total_plus_rebates(&self) -> u64;
|
||||
}
|
||||
|
||||
impl OpenOrdersReserved for OpenOrdersSlim {
|
||||
fn native_coin_reserved(&self) -> u64 {
|
||||
self.native_coin_total - self.native_coin_free
|
||||
impl OpenOrdersAmounts for OpenOrdersSlim {
|
||||
fn native_base_reserved(&self) -> u64 {
|
||||
cm!(self.native_coin_total - self.native_coin_free)
|
||||
}
|
||||
fn native_pc_reserved(&self) -> u64 {
|
||||
self.native_pc_total - self.native_pc_free
|
||||
fn native_quote_reserved(&self) -> u64 {
|
||||
cm!(self.native_pc_total - self.native_pc_free)
|
||||
}
|
||||
fn native_base_free(&self) -> u64 {
|
||||
self.native_coin_free
|
||||
}
|
||||
fn native_quote_free(&self) -> u64 {
|
||||
self.native_pc_free
|
||||
}
|
||||
fn native_quote_free_plus_rebates(&self) -> u64 {
|
||||
cm!(self.native_pc_free + self.referrer_rebates_accrued)
|
||||
}
|
||||
fn native_base_total(&self) -> u64 {
|
||||
self.native_coin_total
|
||||
}
|
||||
fn native_quote_total_plus_rebates(&self) -> u64 {
|
||||
cm!(self.native_pc_total + self.referrer_rebates_accrued)
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenOrdersReserved for OpenOrders {
|
||||
fn native_coin_reserved(&self) -> u64 {
|
||||
self.native_coin_total - self.native_coin_free
|
||||
impl OpenOrdersAmounts for OpenOrders {
|
||||
fn native_base_reserved(&self) -> u64 {
|
||||
cm!(self.native_coin_total - self.native_coin_free)
|
||||
}
|
||||
fn native_pc_reserved(&self) -> u64 {
|
||||
self.native_pc_total - self.native_pc_free
|
||||
fn native_quote_reserved(&self) -> u64 {
|
||||
cm!(self.native_pc_total - self.native_pc_free)
|
||||
}
|
||||
fn native_base_free(&self) -> u64 {
|
||||
self.native_coin_free
|
||||
}
|
||||
fn native_quote_free(&self) -> u64 {
|
||||
self.native_pc_free
|
||||
}
|
||||
fn native_quote_free_plus_rebates(&self) -> u64 {
|
||||
cm!(self.native_pc_free + self.referrer_rebates_accrued)
|
||||
}
|
||||
fn native_base_total(&self) -> u64 {
|
||||
self.native_coin_total
|
||||
}
|
||||
fn native_quote_total_plus_rebates(&self) -> u64 {
|
||||
cm!(self.native_pc_total + self.referrer_rebates_accrued)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,12 +120,16 @@ pub enum Serum3Side {
|
|||
pub struct Serum3PlaceOrder<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(mut, has_one = group)]
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group
|
||||
// owner is checked at #1
|
||||
)]
|
||||
pub account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
|
||||
pub open_orders: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
|
@ -129,20 +168,13 @@ pub struct Serum3PlaceOrder<'info> {
|
|||
/// CHECK: Validated by the serum cpi call
|
||||
pub market_vault_signer: UncheckedAccount<'info>,
|
||||
|
||||
// TODO: do we need to pass both, or just payer?
|
||||
// TODO: if we potentially settle immediately, they all need to be mut?
|
||||
// TODO: Can we reduce the number of accounts by requiring the banks
|
||||
// to be in the remainingAccounts (where they need to be anyway, for
|
||||
// health checks - but they need to be mut)
|
||||
// token_index and bank.vault == vault is validated inline
|
||||
/// The bank that pays for the order, if necessary
|
||||
// token_index and payer_bank.vault == payer_vault is validated inline at #3
|
||||
#[account(mut, has_one = group)]
|
||||
pub quote_bank: AccountLoader<'info, Bank>,
|
||||
pub payer_bank: AccountLoader<'info, Bank>,
|
||||
/// The bank vault that pays for the order, if necessary
|
||||
#[account(mut)]
|
||||
pub quote_vault: Box<Account<'info, TokenAccount>>,
|
||||
#[account(mut, has_one = group)]
|
||||
pub base_bank: AccountLoader<'info, Bank>,
|
||||
#[account(mut)]
|
||||
pub base_vault: Box<Account<'info, TokenAccount>>,
|
||||
pub payer_vault: Box<Account<'info, TokenAccount>>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
@ -166,68 +198,86 @@ pub fn serum3_place_order(
|
|||
//
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
// account constraint #1
|
||||
require!(
|
||||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
// Validate open_orders
|
||||
// Validate open_orders #2
|
||||
require!(
|
||||
account
|
||||
.serum3_orders(serum_market.market_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?
|
||||
.serum3_orders(serum_market.market_index)?
|
||||
.open_orders
|
||||
== ctx.accounts.open_orders.key(),
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
// Validate banks and vaults
|
||||
let quote_bank = ctx.accounts.quote_bank.load()?;
|
||||
require!(
|
||||
quote_bank.vault == ctx.accounts.quote_vault.key(),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(
|
||||
quote_bank.token_index == serum_market.quote_token_index,
|
||||
MangoError::SomeError
|
||||
);
|
||||
let base_bank = ctx.accounts.base_bank.load()?;
|
||||
require!(
|
||||
base_bank.vault == ctx.accounts.base_vault.key(),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(
|
||||
base_bank.token_index == serum_market.base_token_index,
|
||||
MangoError::SomeError
|
||||
);
|
||||
// Validate bank and vault #3
|
||||
let payer_bank = ctx.accounts.payer_bank.load()?;
|
||||
require_keys_eq!(payer_bank.vault, ctx.accounts.payer_vault.key());
|
||||
let payer_token_index = match side {
|
||||
Serum3Side::Bid => serum_market.quote_token_index,
|
||||
Serum3Side::Ask => serum_market.base_token_index,
|
||||
};
|
||||
require_eq!(payer_bank.token_index, payer_token_index);
|
||||
}
|
||||
|
||||
//
|
||||
// Pre-health computation
|
||||
//
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
||||
let pre_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_health))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
//
|
||||
// Before-order tracking
|
||||
//
|
||||
|
||||
let before_base_vault = ctx.accounts.base_vault.amount;
|
||||
let before_quote_vault = ctx.accounts.quote_vault.amount;
|
||||
let before_vault = ctx.accounts.payer_vault.amount;
|
||||
|
||||
let before_oo = {
|
||||
let oo_ai = &ctx.accounts.open_orders.as_ref();
|
||||
let open_orders = load_open_orders_ref(oo_ai)?;
|
||||
OpenOrdersSlim::from_oo(&open_orders)
|
||||
};
|
||||
|
||||
// Provide a readable error message in case the vault doesn't have enough tokens
|
||||
let (vault_amount, needed_amount) = match side {
|
||||
Serum3Side::Ask => (before_base_vault, max_base_qty),
|
||||
Serum3Side::Bid => (before_quote_vault, max_native_quote_qty_including_fees),
|
||||
};
|
||||
if vault_amount < needed_amount {
|
||||
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
|
||||
format!(
|
||||
"bank vault does not have enough tokens, need {} but have {}",
|
||||
needed_amount, vault_amount
|
||||
)
|
||||
});
|
||||
{
|
||||
let base_lot_size = load_market_state(
|
||||
&ctx.accounts.serum_market_external,
|
||||
&ctx.accounts.serum_program.key(),
|
||||
)?
|
||||
.coin_lot_size;
|
||||
|
||||
let needed_amount = match side {
|
||||
Serum3Side::Ask => {
|
||||
cm!(max_base_qty * base_lot_size).saturating_sub(before_oo.native_base_free())
|
||||
}
|
||||
Serum3Side::Bid => {
|
||||
max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free())
|
||||
}
|
||||
};
|
||||
if before_vault < needed_amount {
|
||||
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
|
||||
format!(
|
||||
"bank vault does not have enough tokens, need {} but have {}",
|
||||
needed_amount, before_vault
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: pre-health check
|
||||
|
||||
//
|
||||
// Apply the order to serum. Also immediately settle, in case the order
|
||||
// matched against an existing other order.
|
||||
// Apply the order to serum
|
||||
//
|
||||
let order = serum_dex::instruction::NewOrderInstructionV3 {
|
||||
side: u8::try_from(side).unwrap().try_into().unwrap(),
|
||||
|
@ -242,171 +292,149 @@ pub fn serum3_place_order(
|
|||
client_order_id,
|
||||
limit,
|
||||
};
|
||||
|
||||
let before_oo = {
|
||||
let oo_ai = &ctx.accounts.open_orders.as_ref();
|
||||
let open_orders = load_open_orders_ref(oo_ai)?;
|
||||
OpenOrdersSlim::from_oo(&open_orders)
|
||||
};
|
||||
cpi_place_order(ctx.accounts, order)?;
|
||||
|
||||
{
|
||||
let oo_difference = {
|
||||
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);
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
inc_maybe_loan(
|
||||
serum_market.market_index,
|
||||
&mut account.borrow_mut(),
|
||||
&before_oo,
|
||||
&after_oo,
|
||||
);
|
||||
}
|
||||
|
||||
cpi_settle_funds(ctx.accounts)?;
|
||||
OODifference::new(&before_oo, &after_oo)
|
||||
};
|
||||
|
||||
//
|
||||
// After-order tracking
|
||||
//
|
||||
ctx.accounts.base_vault.reload()?;
|
||||
ctx.accounts.quote_vault.reload()?;
|
||||
let after_base_vault = ctx.accounts.base_vault.amount;
|
||||
let after_quote_vault = ctx.accounts.quote_vault.amount;
|
||||
ctx.accounts.payer_vault.reload()?;
|
||||
let after_vault = ctx.accounts.payer_vault.amount;
|
||||
|
||||
// Charge the difference in vault balances to the user's account
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = {
|
||||
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
|
||||
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
// Placing an order cannot increase vault balance
|
||||
require_gte!(before_vault, after_vault);
|
||||
|
||||
// Charge the difference in vault balance to the user's account
|
||||
let vault_difference = {
|
||||
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
|
||||
apply_vault_difference(
|
||||
&mut account.borrow_mut(),
|
||||
&mut base_bank,
|
||||
after_base_vault,
|
||||
before_base_vault,
|
||||
&mut quote_bank,
|
||||
after_quote_vault,
|
||||
before_quote_vault,
|
||||
serum_market.market_index,
|
||||
&mut payer_bank,
|
||||
after_vault,
|
||||
before_vault,
|
||||
)?
|
||||
};
|
||||
|
||||
//
|
||||
// Health check
|
||||
//
|
||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
|
||||
msg!("health: {}", health);
|
||||
require!(health >= 0, MangoError::HealthMustBePositive);
|
||||
account.fixed.maybe_recover_from_being_liquidated(health);
|
||||
|
||||
vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut());
|
||||
|
||||
if base_loan_origination_fee.is_positive() {
|
||||
emit!(WithdrawLoanOriginationFeeLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: serum_market.base_token_index,
|
||||
loan_origination_fee: base_loan_origination_fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::Serum3PlaceOrder
|
||||
});
|
||||
}
|
||||
if quote_loan_origination_fee.is_positive() {
|
||||
emit!(WithdrawLoanOriginationFeeLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: serum_market.quote_token_index,
|
||||
loan_origination_fee: quote_loan_origination_fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::Serum3PlaceOrder
|
||||
});
|
||||
if let Some((mut health_cache, pre_health)) = pre_health_opt {
|
||||
vault_difference.adjust_health_cache(&mut health_cache)?;
|
||||
oo_difference.adjust_health_cache(&mut health_cache, &serum_market)?;
|
||||
account.check_health_post(&health_cache, pre_health)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// if reserved has increased, then increase cached value by the increase in reserved
|
||||
pub fn inc_maybe_loan(
|
||||
market_index: Serum3MarketIndex,
|
||||
account: &mut MangoAccountRefMut,
|
||||
before_oo: &OpenOrdersSlim,
|
||||
after_oo: &OpenOrdersSlim,
|
||||
) {
|
||||
let serum3_account = account.serum3_orders_mut(market_index).unwrap();
|
||||
|
||||
if after_oo.native_coin_reserved() > before_oo.native_coin_reserved() {
|
||||
let native_coin_reserved_increase =
|
||||
after_oo.native_coin_reserved() - before_oo.native_coin_reserved();
|
||||
serum3_account.previous_native_coin_reserved =
|
||||
cm!(serum3_account.previous_native_coin_reserved + native_coin_reserved_increase);
|
||||
}
|
||||
|
||||
if after_oo.native_pc_reserved() > before_oo.native_pc_reserved() {
|
||||
let reserved_pc_increase = after_oo.native_pc_reserved() - before_oo.native_pc_reserved();
|
||||
serum3_account.previous_native_pc_reserved =
|
||||
cm!(serum3_account.previous_native_pc_reserved + reserved_pc_increase);
|
||||
}
|
||||
pub struct OODifference {
|
||||
reserved_base_change: I80F48,
|
||||
reserved_quote_change: I80F48,
|
||||
free_base_change: I80F48,
|
||||
free_quote_change: I80F48,
|
||||
}
|
||||
|
||||
pub struct VaultDifferenceResult {
|
||||
base_raw_index: usize,
|
||||
base_active: bool,
|
||||
quote_raw_index: usize,
|
||||
quote_active: bool,
|
||||
}
|
||||
|
||||
impl VaultDifferenceResult {
|
||||
pub fn deactivate_inactive_token_accounts(&self, account: &mut MangoAccountRefMut) {
|
||||
if !self.base_active {
|
||||
account.deactivate_token_position(self.base_raw_index);
|
||||
}
|
||||
if !self.quote_active {
|
||||
account.deactivate_token_position(self.quote_raw_index);
|
||||
impl OODifference {
|
||||
pub fn new(before_oo: &OpenOrdersSlim, after_oo: &OpenOrdersSlim) -> Self {
|
||||
Self {
|
||||
reserved_base_change: cm!(I80F48::from(after_oo.native_base_reserved())
|
||||
- I80F48::from(before_oo.native_base_reserved())),
|
||||
reserved_quote_change: cm!(I80F48::from(after_oo.native_quote_reserved())
|
||||
- I80F48::from(before_oo.native_quote_reserved())),
|
||||
free_base_change: cm!(I80F48::from(after_oo.native_base_free())
|
||||
- I80F48::from(before_oo.native_base_free())),
|
||||
free_quote_change: cm!(I80F48::from(after_oo.native_quote_free_plus_rebates())
|
||||
- I80F48::from(before_oo.native_quote_free_plus_rebates())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn adjust_health_cache(
|
||||
&self,
|
||||
health_cache: &mut HealthCache,
|
||||
market: &Serum3Market,
|
||||
) -> Result<()> {
|
||||
health_cache.adjust_serum3_reserved(
|
||||
market.market_index,
|
||||
market.base_token_index,
|
||||
self.reserved_base_change,
|
||||
self.free_base_change,
|
||||
market.quote_token_index,
|
||||
self.reserved_quote_change,
|
||||
self.free_quote_change,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VaultDifference {
|
||||
token_index: TokenIndex,
|
||||
native_change: I80F48,
|
||||
}
|
||||
|
||||
impl VaultDifference {
|
||||
pub fn adjust_health_cache(&self, health_cache: &mut HealthCache) -> Result<()> {
|
||||
health_cache.adjust_token_balance(self.token_index, self.native_change)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Called in settle_funds, place_order, liq_force_cancel to adjust token positions after
|
||||
/// changing the vault balances
|
||||
pub fn apply_vault_difference(
|
||||
account: &mut MangoAccountRefMut,
|
||||
base_bank: &mut Bank,
|
||||
after_base_vault: u64,
|
||||
before_base_vault: u64,
|
||||
quote_bank: &mut Bank,
|
||||
after_quote_vault: u64,
|
||||
before_quote_vault: u64,
|
||||
) -> Result<(VaultDifferenceResult, I80F48, I80F48)> {
|
||||
// TODO: Applying the loan origination fee here may be too early: it should only be
|
||||
// charged if an order executes and the loan materializes? Otherwise MMs that place
|
||||
// an order without having the funds will be charged for each place_order!
|
||||
serum_market_index: Serum3MarketIndex,
|
||||
bank: &mut Bank,
|
||||
vault_after: u64,
|
||||
vault_before: u64,
|
||||
) -> Result<VaultDifference> {
|
||||
let needed_change = cm!(I80F48::from(vault_after) - I80F48::from(vault_before));
|
||||
|
||||
let (base_position, base_raw_index) = account.token_position_mut(base_bank.token_index)?;
|
||||
let base_change = I80F48::from(after_base_vault) - I80F48::from(before_base_vault);
|
||||
let (base_active, base_loan_origination_fee) =
|
||||
base_bank.change_with_fee(base_position, base_change)?;
|
||||
let (position, _) = account.token_position_mut(bank.token_index)?;
|
||||
let native_before = position.native(bank);
|
||||
bank.change_without_fee(position, needed_change)?;
|
||||
let native_after = position.native(bank);
|
||||
let native_change = cm!(native_after - native_before);
|
||||
let new_borrows = native_change
|
||||
.max(native_after)
|
||||
.min(I80F48::ZERO)
|
||||
.abs()
|
||||
.to_num::<u64>();
|
||||
|
||||
let (quote_position, quote_raw_index) = account.token_position_mut(quote_bank.token_index)?;
|
||||
let quote_change = I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault);
|
||||
let (quote_active, quote_loan_origination_fee) =
|
||||
quote_bank.change_with_fee(quote_position, quote_change)?;
|
||||
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
|
||||
} else if bank.token_index == market.quote_token_index {
|
||||
&mut market.quote_borrows_without_fee
|
||||
} else {
|
||||
return Err(error_msg!(
|
||||
"assert failed: apply_vault_difference called with bad token index"
|
||||
));
|
||||
};
|
||||
|
||||
Ok((
|
||||
VaultDifferenceResult {
|
||||
base_raw_index,
|
||||
base_active,
|
||||
quote_raw_index,
|
||||
quote_active,
|
||||
},
|
||||
base_loan_origination_fee,
|
||||
quote_loan_origination_fee,
|
||||
))
|
||||
// Only for place: Add to potential borrow amount
|
||||
let old_value = *borrows_without_fee;
|
||||
*borrows_without_fee = cm!(old_value + new_borrows);
|
||||
|
||||
// Only for settle/liq_force_cancel: Reduce the potential borrow amounts
|
||||
if needed_change > 0 {
|
||||
*borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::<u64>());
|
||||
}
|
||||
|
||||
Ok(VaultDifference {
|
||||
token_index: bank.token_index,
|
||||
native_change,
|
||||
})
|
||||
}
|
||||
|
||||
fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> {
|
||||
use crate::serum3_cpi;
|
||||
|
||||
let order_payer_token_account = match order.side {
|
||||
Side::Bid => &ctx.quote_vault,
|
||||
Side::Ask => &ctx.base_vault,
|
||||
};
|
||||
|
||||
let group = ctx.group.load()?;
|
||||
serum3_cpi::PlaceOrder {
|
||||
program: ctx.serum_program.to_account_info(),
|
||||
|
@ -420,26 +448,8 @@ fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Resu
|
|||
token_program: ctx.token_program.to_account_info(),
|
||||
|
||||
open_orders: ctx.open_orders.to_account_info(),
|
||||
order_payer_token_account: order_payer_token_account.to_account_info(),
|
||||
order_payer_token_account: ctx.payer_vault.to_account_info(),
|
||||
user_authority: ctx.group.to_account_info(),
|
||||
}
|
||||
.call(&group, order)
|
||||
}
|
||||
|
||||
fn cpi_settle_funds(ctx: &Serum3PlaceOrder) -> Result<()> {
|
||||
use crate::serum3_cpi;
|
||||
let group = ctx.group.load()?;
|
||||
serum3_cpi::SettleFunds {
|
||||
program: ctx.serum_program.to_account_info(),
|
||||
market: ctx.serum_market_external.to_account_info(),
|
||||
open_orders: ctx.open_orders.to_account_info(),
|
||||
open_orders_authority: ctx.group.to_account_info(),
|
||||
base_vault: ctx.market_base_vault.to_account_info(),
|
||||
quote_vault: ctx.market_quote_vault.to_account_info(),
|
||||
user_base_wallet: ctx.base_vault.to_account_info(),
|
||||
user_quote_wallet: ctx.quote_vault.to_account_info(),
|
||||
vault_signer: ctx.market_vault_signer.to_account_info(),
|
||||
token_program: ctx.token_program.to_account_info(),
|
||||
}
|
||||
.call(&group)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::state::*;
|
|||
use crate::util::fill_from_str;
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(market_index: Serum3MarketIndex)]
|
||||
pub struct Serum3RegisterMarket<'info> {
|
||||
#[account(
|
||||
mut,
|
||||
|
@ -15,7 +16,6 @@ pub struct Serum3RegisterMarket<'info> {
|
|||
pub group: AccountLoader<'info, Group>,
|
||||
pub admin: Signer<'info>,
|
||||
|
||||
// TODO: limit?
|
||||
/// CHECK: Can register a market for any serum program
|
||||
pub serum_program: UncheckedAccount<'info>,
|
||||
/// CHECK: Can register any serum market
|
||||
|
@ -31,6 +31,17 @@ pub struct Serum3RegisterMarket<'info> {
|
|||
)]
|
||||
pub serum_market: AccountLoader<'info, Serum3Market>,
|
||||
|
||||
/// CHECK: Unused account
|
||||
#[account(
|
||||
init,
|
||||
// block using the same market index twice
|
||||
seeds = [b"Serum3Index".as_ref(), group.key().as_ref(), &market_index.to_le_bytes()],
|
||||
bump,
|
||||
payer = payer,
|
||||
space = 1,
|
||||
)]
|
||||
pub index_reservation: UncheckedAccount<'info>,
|
||||
|
||||
#[account(has_one = group)]
|
||||
pub quote_bank: AccountLoader<'info, Bank>,
|
||||
#[account(has_one = group)]
|
||||
|
@ -42,7 +53,6 @@ pub struct Serum3RegisterMarket<'info> {
|
|||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
// TODO: should this be "configure_serum_market", which allows reconfiguring?
|
||||
pub fn serum3_register_market(
|
||||
ctx: Context<Serum3RegisterMarket>,
|
||||
market_index: Serum3MarketIndex,
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::borrow::BorrowMut;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{Token, TokenAccount};
|
||||
|
||||
|
@ -9,19 +7,23 @@ use crate::error::*;
|
|||
use crate::serum3_cpi::load_open_orders_ref;
|
||||
use crate::state::*;
|
||||
|
||||
use super::{apply_vault_difference, OpenOrdersReserved, OpenOrdersSlim};
|
||||
use super::{apply_vault_difference, OpenOrdersAmounts, OpenOrdersSlim};
|
||||
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Serum3SettleFunds<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(mut, has_one = group)]
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group
|
||||
// owner is checked at #1
|
||||
)]
|
||||
pub account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account
|
||||
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
|
||||
pub open_orders: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
|
@ -48,7 +50,7 @@ pub struct Serum3SettleFunds<'info> {
|
|||
/// CHECK: Validated by the serum cpi call
|
||||
pub market_vault_signer: UncheckedAccount<'info>,
|
||||
|
||||
// token_index and bank.vault == vault is validated inline
|
||||
// token_index and bank.vault == vault is validated inline at #3
|
||||
#[account(mut, has_one = group)]
|
||||
pub quote_bank: AccountLoader<'info, Bank>,
|
||||
#[account(mut)]
|
||||
|
@ -74,22 +76,22 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
|
|||
//
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
// account constraint #1
|
||||
require!(
|
||||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
// Validate open_orders
|
||||
// Validate open_orders #2
|
||||
require!(
|
||||
account
|
||||
.serum3_orders(serum_market.market_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?
|
||||
.serum3_orders(serum_market.market_index)?
|
||||
.open_orders
|
||||
== ctx.accounts.open_orders.key(),
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
// Validate banks and vaults
|
||||
// Validate banks and vaults #3
|
||||
let quote_bank = ctx.accounts.quote_bank.load()?;
|
||||
require!(
|
||||
quote_bank.vault == ctx.accounts.quote_vault.key(),
|
||||
|
@ -111,34 +113,35 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
|
|||
}
|
||||
|
||||
//
|
||||
// Before-order tracking
|
||||
//
|
||||
|
||||
let before_base_vault = ctx.accounts.base_vault.amount;
|
||||
let before_quote_vault = ctx.accounts.quote_vault.amount;
|
||||
|
||||
//
|
||||
// Settle
|
||||
// Charge any open loan origination fees
|
||||
//
|
||||
{
|
||||
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
|
||||
cpi_settle_funds(ctx.accounts)?;
|
||||
|
||||
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
||||
let before_oo = OpenOrdersSlim::from_oo(&open_orders);
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
|
||||
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
charge_maybe_fees(
|
||||
charge_loan_origination_fees(
|
||||
&ctx.accounts.group.key(),
|
||||
&ctx.accounts.account.key(),
|
||||
serum_market.market_index,
|
||||
&mut base_bank,
|
||||
&mut quote_bank,
|
||||
&mut account.borrow_mut(),
|
||||
&after_oo,
|
||||
&before_oo,
|
||||
)?;
|
||||
}
|
||||
|
||||
//
|
||||
// After-order tracking
|
||||
// Settle
|
||||
//
|
||||
let before_base_vault = ctx.accounts.base_vault.amount;
|
||||
let before_quote_vault = ctx.accounts.quote_vault.amount;
|
||||
|
||||
cpi_settle_funds(ctx.accounts)?;
|
||||
|
||||
//
|
||||
// After-settle tracking
|
||||
//
|
||||
{
|
||||
ctx.accounts.base_vault.reload()?;
|
||||
|
@ -146,102 +149,92 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
|
|||
let after_base_vault = ctx.accounts.base_vault.amount;
|
||||
let after_quote_vault = ctx.accounts.quote_vault.amount;
|
||||
|
||||
// Charge the difference in vault balances to the user's account
|
||||
// Settle cannot decrease vault balances
|
||||
require_gte!(after_base_vault, before_base_vault);
|
||||
require_gte!(after_quote_vault, before_quote_vault);
|
||||
|
||||
// Credit the difference in vault balances to the user's account
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
|
||||
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) =
|
||||
apply_vault_difference(
|
||||
&mut account.borrow_mut(),
|
||||
&mut base_bank,
|
||||
after_base_vault,
|
||||
before_base_vault,
|
||||
&mut quote_bank,
|
||||
after_quote_vault,
|
||||
before_quote_vault,
|
||||
)?;
|
||||
vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut());
|
||||
|
||||
if base_loan_origination_fee.is_positive() {
|
||||
emit!(WithdrawLoanOriginationFeeLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: serum_market.base_token_index,
|
||||
loan_origination_fee: base_loan_origination_fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds
|
||||
});
|
||||
}
|
||||
if quote_loan_origination_fee.is_positive() {
|
||||
emit!(WithdrawLoanOriginationFeeLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: serum_market.quote_token_index,
|
||||
loan_origination_fee: quote_loan_origination_fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds
|
||||
});
|
||||
}
|
||||
apply_vault_difference(
|
||||
&mut account.borrow_mut(),
|
||||
serum_market.market_index,
|
||||
&mut base_bank,
|
||||
after_base_vault,
|
||||
before_base_vault,
|
||||
)?;
|
||||
apply_vault_difference(
|
||||
&mut account.borrow_mut(),
|
||||
serum_market.market_index,
|
||||
&mut quote_bank,
|
||||
after_quote_vault,
|
||||
before_quote_vault,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// if reserved is less than cached, charge loan fee on the difference
|
||||
pub fn charge_maybe_fees(
|
||||
// Charge fees if the potential borrows are bigger than the funds on the open orders account
|
||||
pub fn charge_loan_origination_fees(
|
||||
group_pubkey: &Pubkey,
|
||||
account_pubkey: &Pubkey,
|
||||
market_index: Serum3MarketIndex,
|
||||
coin_bank: &mut Bank,
|
||||
pc_bank: &mut Bank,
|
||||
base_bank: &mut Bank,
|
||||
quote_bank: &mut Bank,
|
||||
account: &mut MangoAccountRefMut,
|
||||
after_oo: &OpenOrdersSlim,
|
||||
before_oo: &OpenOrdersSlim,
|
||||
) -> Result<()> {
|
||||
let serum3_account = account.serum3_orders_mut(market_index).unwrap();
|
||||
|
||||
let maybe_actualized_coin_loan = I80F48::from_num::<u64>(
|
||||
let oo_base_total = before_oo.native_base_total();
|
||||
let actualized_base_loan = I80F48::from_num(
|
||||
serum3_account
|
||||
.previous_native_coin_reserved
|
||||
.saturating_sub(after_oo.native_coin_reserved()),
|
||||
.base_borrows_without_fee
|
||||
.saturating_sub(oo_base_total),
|
||||
);
|
||||
if actualized_base_loan > 0 {
|
||||
serum3_account.base_borrows_without_fee = oo_base_total;
|
||||
|
||||
if maybe_actualized_coin_loan > 0 {
|
||||
serum3_account.previous_native_coin_reserved = after_oo.native_coin_reserved();
|
||||
// now that the loan is actually materialized, charge the loan origination fee
|
||||
// note: the withdraw has already happened while placing the order
|
||||
let base_token_account = account.token_position_mut(base_bank.token_index)?.0;
|
||||
let (_, fee) =
|
||||
base_bank.withdraw_loan_origination_fee(base_token_account, actualized_base_loan)?;
|
||||
|
||||
// loan origination fees
|
||||
let coin_token_account = account.token_position_mut(coin_bank.token_index)?.0;
|
||||
let coin_token_native = coin_token_account.native(coin_bank);
|
||||
|
||||
if coin_token_native.is_negative() {
|
||||
let actualized_loan = coin_token_native.abs().min(maybe_actualized_coin_loan);
|
||||
// note: the withdraw has already happened while placing the order
|
||||
// now that the loan is actually materialized (since the fill having taken place)
|
||||
// charge the loan origination fee
|
||||
coin_bank
|
||||
.borrow_mut()
|
||||
.withdraw_loan_origination_fee(coin_token_account, actualized_loan)?;
|
||||
}
|
||||
emit!(WithdrawLoanOriginationFeeLog {
|
||||
mango_group: *group_pubkey,
|
||||
mango_account: *account_pubkey,
|
||||
token_index: base_bank.token_index,
|
||||
loan_origination_fee: fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds,
|
||||
});
|
||||
}
|
||||
|
||||
let serum3_account = account.serum3_orders_mut(market_index).unwrap();
|
||||
let maybe_actualized_pc_loan = I80F48::from_num::<u64>(
|
||||
let oo_quote_total = before_oo.native_quote_total_plus_rebates();
|
||||
let actualized_quote_loan = I80F48::from_num::<u64>(
|
||||
serum3_account
|
||||
.previous_native_pc_reserved
|
||||
.saturating_sub(after_oo.native_pc_reserved()),
|
||||
.quote_borrows_without_fee
|
||||
.saturating_sub(oo_quote_total),
|
||||
);
|
||||
if actualized_quote_loan > 0 {
|
||||
serum3_account.quote_borrows_without_fee = oo_quote_total;
|
||||
|
||||
if maybe_actualized_pc_loan > 0 {
|
||||
serum3_account.previous_native_pc_reserved = after_oo.native_pc_reserved();
|
||||
// now that the loan is actually materialized, charge the loan origination fee
|
||||
// note: the withdraw has already happened while placing the order
|
||||
let quote_token_account = account.token_position_mut(quote_bank.token_index)?.0;
|
||||
let (_, fee) =
|
||||
quote_bank.withdraw_loan_origination_fee(quote_token_account, actualized_quote_loan)?;
|
||||
|
||||
// loan origination fees
|
||||
let pc_token_account = account.token_position_mut(pc_bank.token_index)?.0;
|
||||
let pc_token_native = pc_token_account.native(pc_bank);
|
||||
|
||||
if pc_token_native.is_negative() {
|
||||
let actualized_loan = pc_token_native.abs().min(maybe_actualized_pc_loan);
|
||||
// note: the withdraw has already happened while placing the order
|
||||
// now that the loan is actually materialized (since the fill having taken place)
|
||||
// charge the loan origination fee
|
||||
pc_bank
|
||||
.borrow_mut()
|
||||
.withdraw_loan_origination_fee(pc_token_account, actualized_loan)?;
|
||||
}
|
||||
emit!(WithdrawLoanOriginationFeeLog {
|
||||
mango_group: *group_pubkey,
|
||||
mango_account: *account_pubkey,
|
||||
token_index: quote_bank.token_index,
|
||||
loan_origination_fee: fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -4,6 +4,7 @@ use anchor_spl::token::Token;
|
|||
use anchor_spl::token::TokenAccount;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::accounts_zerocopy::AccountInfoRef;
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use crate::util::checked_math as cm;
|
||||
|
@ -21,6 +22,7 @@ pub struct TokenDeposit<'info> {
|
|||
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
|
||||
)]
|
||||
|
@ -29,6 +31,9 @@ pub struct TokenDeposit<'info> {
|
|||
#[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<Account<'info, TokenAccount>>,
|
||||
pub token_authority: Signer<'info>,
|
||||
|
@ -56,7 +61,7 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
|
|||
// 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 (position, raw_token_index, _active_token_index) =
|
||||
account.ensure_token_position(token_index)?;
|
||||
|
||||
let amount_i80f48 = I80F48::from(amount);
|
||||
|
@ -69,10 +74,8 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
|
|||
token::transfer(ctx.accounts.transfer_ctx(), amount)?;
|
||||
|
||||
let indexed_position = position.indexed_position;
|
||||
|
||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let (bank, oracle_price) =
|
||||
retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?;
|
||||
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::<i64>();
|
||||
|
@ -91,10 +94,17 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
|
|||
//
|
||||
// Health computation
|
||||
//
|
||||
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);
|
||||
// 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
|
||||
|
|
|
@ -4,8 +4,9 @@ use anchor_spl::token::{self, CloseAccount, Token, TokenAccount};
|
|||
use crate::{accounts_zerocopy::LoadZeroCopyRef, state::*};
|
||||
use anchor_lang::AccountsClose;
|
||||
|
||||
/// In addition to these accounts, there must be remaining_accounts:
|
||||
/// all n pairs of bank and its corresponding vault account for a token
|
||||
#[derive(Accounts)]
|
||||
#[instruction(token_index: TokenIndex)]
|
||||
pub struct TokenDeregister<'info> {
|
||||
#[account(
|
||||
constraint = group.load()?.is_testing(),
|
||||
|
@ -18,7 +19,6 @@ pub struct TokenDeregister<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
constraint = mint_info.load()?.token_index == token_index,
|
||||
close = sol_destination
|
||||
)]
|
||||
pub mint_info: AccountLoader<'info, MintInfo>,
|
||||
|
@ -36,7 +36,6 @@ pub struct TokenDeregister<'info> {
|
|||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn token_deregister<'key, 'accounts, 'remaining, 'info>(
|
||||
ctx: Context<'key, 'accounts, 'remaining, 'info, TokenDeregister<'info>>,
|
||||
token_index: TokenIndex,
|
||||
) -> Result<()> {
|
||||
let mint_info = ctx.accounts.mint_info.load()?;
|
||||
{
|
||||
|
@ -61,7 +60,7 @@ pub fn token_deregister<'key, 'accounts, 'remaining, 'info>(
|
|||
{
|
||||
let bank = bank_ai.load::<Bank>()?;
|
||||
require_keys_eq!(bank.group, ctx.accounts.group.key());
|
||||
require_eq!(bank.token_index, token_index);
|
||||
require_eq!(bank.token_index, mint_info.token_index);
|
||||
require_keys_eq!(bank.vault, vault_ai.key());
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::logs::{UpdateIndexLog, UpdateRateLog};
|
|||
use crate::state::HOUR;
|
||||
use crate::{
|
||||
accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef},
|
||||
state::{oracle_price, Bank, Group, MintInfo},
|
||||
state::{Bank, Group, MintInfo},
|
||||
};
|
||||
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
|
||||
use anchor_lang::Discriminator;
|
||||
|
@ -106,11 +106,8 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
|||
now_ts,
|
||||
);
|
||||
|
||||
let price = oracle_price(
|
||||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||
some_bank.oracle_config.conf_filter,
|
||||
some_bank.mint_decimals,
|
||||
)?;
|
||||
let price =
|
||||
some_bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
||||
emit!(UpdateIndexLog {
|
||||
mango_group: mint_info.group.key(),
|
||||
token_index: mint_info.token_index,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
|
@ -9,7 +10,6 @@ use fixed::types::I80F48;
|
|||
use crate::logs::{
|
||||
LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanOriginationFeeLog, WithdrawLog,
|
||||
};
|
||||
use crate::state::new_fixed_order_account_retriever;
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
#[derive(Accounts)]
|
||||
|
@ -24,6 +24,7 @@ pub struct TokenWithdraw<'info> {
|
|||
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
|
||||
)]
|
||||
|
@ -32,6 +33,9 @@ pub struct TokenWithdraw<'info> {
|
|||
#[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<Account<'info, TokenAccount>>,
|
||||
|
||||
|
@ -56,90 +60,92 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
let group = ctx.accounts.group.load()?;
|
||||
let token_index = ctx.accounts.bank.load()?.token_index;
|
||||
|
||||
// Get the account's position for that token index
|
||||
// Create the account's position for that token index
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let (_, raw_token_index, _) = account.ensure_token_position(token_index)?;
|
||||
|
||||
let (position, raw_token_index, active_token_index) =
|
||||
account.ensure_token_position(token_index)?;
|
||||
|
||||
// The bank will also be passed in remainingAccounts. Use an explicit scope
|
||||
// to drop the &mut before we borrow it immutably again later.
|
||||
let (position_is_active, amount_i80f48, loan_origination_fee) = {
|
||||
let mut bank = ctx.accounts.bank.load_mut()?;
|
||||
let native_position = position.native(&bank);
|
||||
|
||||
// Handle amount special case for withdrawing everything
|
||||
let amount = if amount == u64::MAX && !allow_borrow {
|
||||
if native_position.is_positive() {
|
||||
// TODO: This rounding may mean that if we deposit and immediately withdraw
|
||||
// we can't withdraw the full amount!
|
||||
native_position.floor().to_num::<u64>()
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
amount
|
||||
};
|
||||
|
||||
require!(
|
||||
allow_borrow || amount <= native_position,
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
let amount_i80f48 = I80F48::from(amount);
|
||||
|
||||
// Update the bank and position
|
||||
let (position_is_active, loan_origination_fee) =
|
||||
bank.withdraw_with_fee(position, amount_i80f48)?;
|
||||
|
||||
// Provide a readable error message in case the vault doesn't have enough tokens
|
||||
if ctx.accounts.vault.amount < amount {
|
||||
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
|
||||
format!(
|
||||
"bank vault does not have enough tokens, need {} but have {}",
|
||||
amount, ctx.accounts.vault.amount
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// Transfer the actual tokens
|
||||
let group_seeds = group_seeds!(group);
|
||||
token::transfer(
|
||||
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
|
||||
amount,
|
||||
)?;
|
||||
|
||||
(position_is_active, amount_i80f48, loan_origination_fee)
|
||||
// Health check _after_ the token position is guaranteed to exist
|
||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
||||
let pre_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_health))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let indexed_position = position.indexed_position;
|
||||
let mut bank = ctx.accounts.bank.load_mut()?;
|
||||
let position = account.token_position_mut_by_raw_index(raw_token_index);
|
||||
let native_position = position.native(&bank);
|
||||
|
||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let (bank, oracle_price) =
|
||||
retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?;
|
||||
// Handle amount special case for withdrawing everything
|
||||
let amount = if amount == u64::MAX && !allow_borrow {
|
||||
if native_position.is_positive() {
|
||||
// TODO: This rounding may mean that if we deposit and immediately withdraw
|
||||
// we can't withdraw the full amount!
|
||||
native_position.floor().to_num::<u64>()
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
amount
|
||||
};
|
||||
|
||||
// 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::<i64>();
|
||||
account.fixed.net_deposits = cm!(account.fixed.net_deposits - amount_usd);
|
||||
require!(
|
||||
allow_borrow || amount <= native_position,
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
let amount_i80f48 = I80F48::from(amount);
|
||||
|
||||
// Update the bank and position
|
||||
let (position_is_active, loan_origination_fee) =
|
||||
bank.withdraw_with_fee(position, amount_i80f48)?;
|
||||
|
||||
// Provide a readable error message in case the vault doesn't have enough tokens
|
||||
if ctx.accounts.vault.amount < amount {
|
||||
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
|
||||
format!(
|
||||
"bank vault does not have enough tokens, need {} but have {}",
|
||||
amount, ctx.accounts.vault.amount
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// Transfer the actual tokens
|
||||
let group_seeds = group_seeds!(group);
|
||||
token::transfer(
|
||||
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
|
||||
amount,
|
||||
)?;
|
||||
|
||||
let native_position_after = position.native(&bank);
|
||||
let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index,
|
||||
indexed_position: indexed_position.to_bits(),
|
||||
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)
|
||||
let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::<i64>();
|
||||
account.fixed.net_deposits = cm!(account.fixed.net_deposits - amount_usd);
|
||||
|
||||
//
|
||||
// Health check
|
||||
//
|
||||
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)
|
||||
.context("post-withdraw init health")?;
|
||||
msg!("health: {}", health);
|
||||
require!(health >= 0, MangoError::HealthMustBePositive);
|
||||
account.fixed.maybe_recover_from_being_liquidated(health);
|
||||
if let Some((mut health_cache, pre_health)) = pre_health_opt {
|
||||
health_cache
|
||||
.adjust_token_balance(token_index, cm!(native_position_after - native_position))?;
|
||||
account.check_health_post(&health_cache, pre_health)?;
|
||||
}
|
||||
|
||||
//
|
||||
// Deactivate the position only after the health check because the user passed in
|
||||
|
|
|
@ -142,9 +142,8 @@ pub mod mango_v4 {
|
|||
|
||||
pub fn token_deregister<'key, 'accounts, 'remaining, 'info>(
|
||||
ctx: Context<'key, 'accounts, 'remaining, 'info, TokenDeregister<'info>>,
|
||||
token_index: TokenIndex,
|
||||
) -> Result<()> {
|
||||
instructions::token_deregister(ctx, token_index)
|
||||
instructions::token_deregister(ctx)
|
||||
}
|
||||
|
||||
pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Result<()> {
|
||||
|
@ -236,6 +235,18 @@ pub mod mango_v4 {
|
|||
instructions::flash_loan_end(ctx, flash_loan_type)
|
||||
}
|
||||
|
||||
pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>(
|
||||
ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionBegin<'info>>,
|
||||
) -> Result<()> {
|
||||
instructions::health_region_begin(ctx)
|
||||
}
|
||||
|
||||
pub fn health_region_end<'key, 'accounts, 'remaining, 'info>(
|
||||
ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionEnd<'info>>,
|
||||
) -> Result<()> {
|
||||
instructions::health_region_end(ctx)
|
||||
}
|
||||
|
||||
///
|
||||
/// Serum
|
||||
///
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use super::{OracleConfig, TokenIndex, TokenPosition};
|
||||
use crate::accounts_zerocopy::KeyedAccountReader;
|
||||
use crate::state::oracle;
|
||||
use crate::util;
|
||||
use crate::util::checked_math as cm;
|
||||
use anchor_lang::prelude::*;
|
||||
|
@ -274,7 +276,7 @@ impl Bank {
|
|||
let (position_is_active, _) =
|
||||
self.withdraw_internal(position, native_amount, false, !position.is_in_use())?;
|
||||
|
||||
return Ok(position_is_active);
|
||||
Ok(position_is_active)
|
||||
}
|
||||
|
||||
/// Like `withdraw_without_fee()` but allows dusting of in-use token accounts.
|
||||
|
@ -285,9 +287,8 @@ impl Bank {
|
|||
position: &mut TokenPosition,
|
||||
native_amount: I80F48,
|
||||
) -> Result<bool> {
|
||||
Ok(self
|
||||
.withdraw_internal(position, native_amount, false, true)
|
||||
.map(|(not_dusted, _)| not_dusted || position.is_in_use())?)
|
||||
self.withdraw_internal(position, native_amount, false, true)
|
||||
.map(|(not_dusted, _)| not_dusted || position.is_in_use())
|
||||
}
|
||||
|
||||
/// Withdraws `native_amount` while applying the loan origination fee if a borrow is created.
|
||||
|
@ -545,6 +546,15 @@ impl Bank {
|
|||
(self.rate0, self.rate1, self.max_rate)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn oracle_price(&self, oracle_acc: &impl KeyedAccountReader) -> Result<I80F48> {
|
||||
require_keys_eq!(self.oracle, *oracle_acc.key());
|
||||
oracle::oracle_price(
|
||||
oracle_acc,
|
||||
self.oracle_config.conf_filter,
|
||||
self.mint_decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
|
|
@ -44,7 +44,7 @@ impl Group {
|
|||
}
|
||||
|
||||
pub fn multiple_banks_supported(&self) -> bool {
|
||||
self.is_testing() || self.version > 0
|
||||
self.is_testing() || self.version > 1
|
||||
}
|
||||
|
||||
pub fn serum3_supported(&self) -> bool {
|
||||
|
@ -52,7 +52,7 @@ impl Group {
|
|||
}
|
||||
|
||||
pub fn perps_supported(&self) -> bool {
|
||||
self.is_testing() || self.version > 0
|
||||
self.is_testing() || self.version > 1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,10 @@ use std::collections::HashMap;
|
|||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::*;
|
||||
use crate::serum3_cpi;
|
||||
use crate::state::{oracle_price, Bank, PerpMarket, PerpMarketIndex, TokenIndex};
|
||||
use crate::state::{
|
||||
Bank, MangoAccountFixed, PerpMarket, PerpMarketIndex, PerpPosition, Serum3MarketIndex,
|
||||
TokenIndex,
|
||||
};
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
use super::MangoAccountRef;
|
||||
|
@ -89,8 +92,7 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
|
|||
|
||||
fn oracle_price(&self, account_index: usize, bank: &Bank) -> Result<I80F48> {
|
||||
let oracle = &self.ais[cm!(self.n_banks + account_index)];
|
||||
require_keys_eq!(bank.oracle, *oracle.key());
|
||||
oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)
|
||||
bank.oracle_price(oracle)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -275,8 +277,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
let index = self.bank_index(token_index1)?;
|
||||
let bank = self.banks[index].load_mut_fully_unchecked::<Bank>()?;
|
||||
let oracle = &self.oracles[index];
|
||||
require_keys_eq!(bank.oracle, *oracle.key);
|
||||
let price = oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?;
|
||||
let price = bank.oracle_price(oracle)?;
|
||||
return Ok((bank, price, None));
|
||||
}
|
||||
let index1 = self.bank_index(token_index1)?;
|
||||
|
@ -294,12 +295,8 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::<Bank>()?;
|
||||
let oracle1 = &self.oracles[first];
|
||||
let oracle2 = &self.oracles[second];
|
||||
require_keys_eq!(bank1.oracle, *oracle1.key);
|
||||
require_keys_eq!(bank2.oracle, *oracle2.key);
|
||||
let mint_decimals1 = bank1.mint_decimals;
|
||||
let mint_decimals2 = bank2.mint_decimals;
|
||||
let price1 = oracle_price(oracle1, bank1.oracle_config.conf_filter, mint_decimals1)?;
|
||||
let price2 = oracle_price(oracle2, bank2.oracle_config.conf_filter, mint_decimals2)?;
|
||||
let price1 = bank1.oracle_price(oracle1)?;
|
||||
let price2 = bank2.oracle_price(oracle2)?;
|
||||
if swap {
|
||||
Ok((bank2, price2, Some((bank1, price1))))
|
||||
} else {
|
||||
|
@ -311,11 +308,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
let index = self.bank_index(token_index)?;
|
||||
let bank = self.banks[index].load_fully_unchecked::<Bank>()?;
|
||||
let oracle = &self.oracles[index];
|
||||
require_keys_eq!(bank.oracle, *oracle.key);
|
||||
Ok((
|
||||
bank,
|
||||
oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?,
|
||||
))
|
||||
Ok((bank, bank.oracle_price(oracle)?))
|
||||
}
|
||||
|
||||
pub fn scanned_perp_market(&self, perp_market_index: PerpMarketIndex) -> Result<&PerpMarket> {
|
||||
|
@ -437,6 +430,7 @@ pub struct Serum3Info {
|
|||
reserved: I80F48,
|
||||
base_index: usize,
|
||||
quote_index: usize,
|
||||
market_index: Serum3MarketIndex,
|
||||
}
|
||||
|
||||
impl Serum3Info {
|
||||
|
@ -480,6 +474,7 @@ impl Serum3Info {
|
|||
|
||||
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
|
||||
pub struct PerpInfo {
|
||||
perp_market_index: PerpMarketIndex,
|
||||
maint_asset_weight: I80F48,
|
||||
init_asset_weight: I80F48,
|
||||
maint_liab_weight: I80F48,
|
||||
|
@ -491,6 +486,93 @@ pub struct PerpInfo {
|
|||
}
|
||||
|
||||
impl PerpInfo {
|
||||
fn new(
|
||||
perp_position: &PerpPosition,
|
||||
perp_market: &PerpMarket,
|
||||
token_infos: &[TokenInfo],
|
||||
) -> Result<Self> {
|
||||
// find the TokenInfos for the market's base and quote tokens
|
||||
let base_index = find_token_info_index(token_infos, perp_market.base_token_index)?;
|
||||
// TODO: base_index could be unset
|
||||
let base_info = &token_infos[base_index];
|
||||
|
||||
let base_lot_size = I80F48::from(perp_market.base_lot_size);
|
||||
|
||||
let base_lots = cm!(perp_position.base_position_lots + perp_position.taker_base_lots);
|
||||
let taker_quote = I80F48::from(cm!(
|
||||
perp_position.taker_quote_lots * perp_market.quote_lot_size
|
||||
));
|
||||
let quote_current = cm!(perp_position.quote_position_native + taker_quote);
|
||||
|
||||
// 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 * base_lot_size * price`.
|
||||
// The health for this case is:
|
||||
// (weighted(base_lots + bids) - bids) * base_lot_size * price + quote
|
||||
// 2. The price goes high and all asks execute, converting to quote.
|
||||
// The health for this case is:
|
||||
// (weighted(base_lots - asks) + asks) * base_lot_size * price + quote
|
||||
//
|
||||
// Comparing these makes it clear we need to pick the worse subfactor
|
||||
// weighted(base_lots + bids) - bids =: scenario1
|
||||
// or
|
||||
// weighted(base_lots - 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 - init_asset_weight) / (init_liab_weight - 1)
|
||||
// == (1 - maint_asset_weight) / (maint_liab_weight - 1)
|
||||
//
|
||||
// Derivation:
|
||||
// Set asks_net_lots := base_lots - asks, bids_net_lots := base_lots + bids.
|
||||
// Now
|
||||
// scenario1 = weighted(bids_net_lots) - bids_net_lots + base_lots and
|
||||
// scenario2 = weighted(asks_net_lots) - asks_net_lots + base_lots
|
||||
// So with expanding weigthed(a) = weight_factor_for_a * a, the question
|
||||
// scenario1 < scenario2
|
||||
// becomes:
|
||||
// (weight_factor_for_bids_net_lots - 1) * bids_net_lots
|
||||
// < (weight_factor_for_asks_net_lots - 1) * asks_net_lots
|
||||
// Since asks_net_lots < 0 and bids_net_lots > 0 is the only interesting case, (P) follows.
|
||||
//
|
||||
// We satisfy (P) by requiring
|
||||
// asset_weight = 1 - x and liab_weight = 1 + x
|
||||
//
|
||||
// And with that assumption the scenario choice condition further simplifies to:
|
||||
// scenario1 < scenario2
|
||||
// iff abs(bids_net_lots) > abs(asks_net_lots)
|
||||
let bids_net_lots = cm!(base_lots + perp_position.bids_base_lots);
|
||||
let asks_net_lots = cm!(base_lots - perp_position.asks_base_lots);
|
||||
|
||||
let lots_to_quote = base_lot_size * base_info.oracle_price;
|
||||
let base;
|
||||
let quote;
|
||||
if cm!(bids_net_lots.abs()) > cm!(asks_net_lots.abs()) {
|
||||
let bids_net_lots = I80F48::from(bids_net_lots);
|
||||
let bids_base_lots = I80F48::from(perp_position.bids_base_lots);
|
||||
base = cm!(bids_net_lots * lots_to_quote);
|
||||
quote = cm!(quote_current - bids_base_lots * lots_to_quote);
|
||||
} else {
|
||||
let asks_net_lots = I80F48::from(asks_net_lots);
|
||||
let asks_base_lots = I80F48::from(perp_position.asks_base_lots);
|
||||
base = cm!(asks_net_lots * lots_to_quote);
|
||||
quote = cm!(quote_current + asks_base_lots * lots_to_quote);
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
perp_market_index: perp_market.perp_market_index,
|
||||
init_asset_weight: perp_market.init_asset_weight,
|
||||
init_liab_weight: perp_market.init_liab_weight,
|
||||
maint_asset_weight: perp_market.maint_asset_weight,
|
||||
maint_liab_weight: perp_market.maint_liab_weight,
|
||||
base,
|
||||
quote,
|
||||
})
|
||||
}
|
||||
|
||||
/// Total health contribution from perp balances
|
||||
///
|
||||
/// Due to isolation of perp markets, users may never borrow against perp
|
||||
|
@ -536,16 +618,107 @@ impl HealthCache {
|
|||
health
|
||||
}
|
||||
|
||||
pub fn check_health_pre(&self, account: &mut MangoAccountFixed) -> Result<I80F48> {
|
||||
let pre_health = self.health(HealthType::Init);
|
||||
msg!("pre_health: {}", pre_health);
|
||||
account.maybe_recover_from_being_liquidated(pre_health);
|
||||
require!(!account.being_liquidated(), MangoError::BeingLiquidated);
|
||||
Ok(pre_health)
|
||||
}
|
||||
|
||||
pub fn check_health_post(
|
||||
&self,
|
||||
account: &mut MangoAccountFixed,
|
||||
pre_health: I80F48,
|
||||
) -> Result<()> {
|
||||
let post_health = self.health(HealthType::Init);
|
||||
msg!("post_health: {}", post_health);
|
||||
require!(
|
||||
post_health >= 0 || post_health > pre_health,
|
||||
MangoError::HealthMustBePositiveOrIncrease
|
||||
);
|
||||
account.maybe_recover_from_being_liquidated(post_health);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn token_entry_index(&mut self, token_index: TokenIndex) -> Result<usize> {
|
||||
self.token_infos
|
||||
.iter()
|
||||
.position(|t| t.token_index == token_index)
|
||||
.ok_or_else(|| error_msg!("token index {} not found", token_index))
|
||||
}
|
||||
|
||||
pub fn adjust_token_balance(&mut self, token_index: TokenIndex, change: I80F48) -> Result<()> {
|
||||
let mut entry = self
|
||||
.token_infos
|
||||
.iter_mut()
|
||||
.find(|t| t.token_index == token_index)
|
||||
.ok_or_else(|| error_msg!("token index {} not found", token_index))?;
|
||||
let entry_index = self.token_entry_index(token_index)?;
|
||||
let mut entry = &mut self.token_infos[entry_index];
|
||||
entry.balance = cm!(entry.balance + change * entry.oracle_price);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn adjust_serum3_reserved(
|
||||
&mut self,
|
||||
market_index: Serum3MarketIndex,
|
||||
base_token_index: TokenIndex,
|
||||
reserved_base_change: I80F48,
|
||||
free_base_change: I80F48,
|
||||
quote_token_index: TokenIndex,
|
||||
reserved_quote_change: I80F48,
|
||||
free_quote_change: I80F48,
|
||||
) -> Result<()> {
|
||||
let base_entry_index = self.token_entry_index(base_token_index)?;
|
||||
let quote_entry_index = self.token_entry_index(quote_token_index)?;
|
||||
|
||||
// Compute the total reserved amount change in health reference units
|
||||
let mut reserved_amount;
|
||||
{
|
||||
let base_entry = &mut self.token_infos[base_entry_index];
|
||||
reserved_amount = cm!(reserved_base_change * base_entry.oracle_price);
|
||||
}
|
||||
{
|
||||
let quote_entry = &mut self.token_infos[quote_entry_index];
|
||||
reserved_amount =
|
||||
cm!(reserved_amount + reserved_quote_change * quote_entry.oracle_price);
|
||||
}
|
||||
|
||||
// Apply it to the tokens
|
||||
{
|
||||
let base_entry = &mut self.token_infos[base_entry_index];
|
||||
base_entry.serum3_max_reserved = cm!(base_entry.serum3_max_reserved + reserved_amount);
|
||||
base_entry.balance =
|
||||
cm!(base_entry.balance + free_base_change * base_entry.oracle_price);
|
||||
}
|
||||
{
|
||||
let quote_entry = &mut self.token_infos[quote_entry_index];
|
||||
quote_entry.serum3_max_reserved =
|
||||
cm!(quote_entry.serum3_max_reserved + reserved_amount);
|
||||
quote_entry.balance =
|
||||
cm!(quote_entry.balance + free_quote_change * quote_entry.oracle_price);
|
||||
}
|
||||
|
||||
// Apply it to the serum3 info
|
||||
let market_entry = self
|
||||
.serum3_infos
|
||||
.iter_mut()
|
||||
.find(|m| m.market_index == market_index)
|
||||
.ok_or_else(|| error_msg!("serum3 market {} not found", market_index))?;
|
||||
market_entry.reserved = cm!(market_entry.reserved + reserved_amount);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recompute_perp_info(
|
||||
&mut self,
|
||||
perp_position: &PerpPosition,
|
||||
perp_market: &PerpMarket,
|
||||
) -> Result<()> {
|
||||
let perp_entry = self
|
||||
.perp_infos
|
||||
.iter_mut()
|
||||
.find(|m| m.perp_market_index == perp_market.perp_market_index)
|
||||
.ok_or_else(|| error_msg!("perp market {} not found", perp_market.perp_market_index))?;
|
||||
*perp_entry = PerpInfo::new(perp_position, perp_market, &self.token_infos)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_liquidatable_assets(&self) -> bool {
|
||||
let spot_liquidatable = self
|
||||
.token_infos
|
||||
|
@ -842,95 +1015,17 @@ pub fn new_health_cache(
|
|||
reserved: reserved_balance,
|
||||
base_index,
|
||||
quote_index,
|
||||
market_index: serum_account.market_index,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: also account for perp funding in health
|
||||
// health contribution from perp accounts
|
||||
let mut perp_infos = Vec::with_capacity(account.active_perp_positions().count());
|
||||
for (i, perp_account) in account.active_perp_positions().enumerate() {
|
||||
for (i, perp_position) in account.active_perp_positions().enumerate() {
|
||||
let perp_market =
|
||||
retriever.perp_market(&account.fixed.group, i, perp_account.market_index)?;
|
||||
|
||||
// find the TokenInfos for the market's base and quote tokens
|
||||
let base_index = find_token_info_index(&token_infos, perp_market.base_token_index)?;
|
||||
// TODO: base_index could be unset
|
||||
let base_info = &token_infos[base_index];
|
||||
|
||||
let base_lot_size = I80F48::from(perp_market.base_lot_size);
|
||||
|
||||
let base_lots = cm!(perp_account.base_position_lots + perp_account.taker_base_lots);
|
||||
let taker_quote = I80F48::from(cm!(
|
||||
perp_account.taker_quote_lots * perp_market.quote_lot_size
|
||||
));
|
||||
let quote_current = cm!(perp_account.quote_position_native + taker_quote);
|
||||
|
||||
// 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 * base_lot_size * price`.
|
||||
// The health for this case is:
|
||||
// (weighted(base_lots + bids) - bids) * base_lot_size * price + quote
|
||||
// 2. The price goes high and all asks execute, converting to quote.
|
||||
// The health for this case is:
|
||||
// (weighted(base_lots - asks) + asks) * base_lot_size * price + quote
|
||||
//
|
||||
// Comparing these makes it clear we need to pick the worse subfactor
|
||||
// weighted(base_lots + bids) - bids =: scenario1
|
||||
// or
|
||||
// weighted(base_lots - 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 - init_asset_weight) / (init_liab_weight - 1)
|
||||
// == (1 - maint_asset_weight) / (maint_liab_weight - 1)
|
||||
//
|
||||
// Derivation:
|
||||
// Set asks_net_lots := base_lots - asks, bids_net_lots := base_lots + bids.
|
||||
// Now
|
||||
// scenario1 = weighted(bids_net_lots) - bids_net_lots + base_lots and
|
||||
// scenario2 = weighted(asks_net_lots) - asks_net_lots + base_lots
|
||||
// So with expanding weigthed(a) = weight_factor_for_a * a, the question
|
||||
// scenario1 < scenario2
|
||||
// becomes:
|
||||
// (weight_factor_for_bids_net_lots - 1) * bids_net_lots
|
||||
// < (weight_factor_for_asks_net_lots - 1) * asks_net_lots
|
||||
// Since asks_net_lots < 0 and bids_net_lots > 0 is the only interesting case, (P) follows.
|
||||
//
|
||||
// We satisfy (P) by requiring
|
||||
// asset_weight = 1 - x and liab_weight = 1 + x
|
||||
//
|
||||
// And with that assumption the scenario choice condition further simplifies to:
|
||||
// scenario1 < scenario2
|
||||
// iff abs(bids_net_lots) > abs(asks_net_lots)
|
||||
let bids_net_lots = cm!(base_lots + perp_account.bids_base_lots);
|
||||
let asks_net_lots = cm!(base_lots - perp_account.asks_base_lots);
|
||||
|
||||
let lots_to_quote = base_lot_size * base_info.oracle_price;
|
||||
let base;
|
||||
let quote;
|
||||
if cm!(bids_net_lots.abs()) > cm!(asks_net_lots.abs()) {
|
||||
let bids_net_lots = I80F48::from(bids_net_lots);
|
||||
let bids_base_lots = I80F48::from(perp_account.bids_base_lots);
|
||||
base = cm!(bids_net_lots * lots_to_quote);
|
||||
quote = cm!(quote_current - bids_base_lots * lots_to_quote);
|
||||
} else {
|
||||
let asks_net_lots = I80F48::from(asks_net_lots);
|
||||
let asks_base_lots = I80F48::from(perp_account.asks_base_lots);
|
||||
base = cm!(asks_net_lots * lots_to_quote);
|
||||
quote = cm!(quote_current + asks_base_lots * lots_to_quote);
|
||||
};
|
||||
|
||||
perp_infos.push(PerpInfo {
|
||||
init_asset_weight: perp_market.init_asset_weight,
|
||||
init_liab_weight: perp_market.init_liab_weight,
|
||||
maint_asset_weight: perp_market.maint_asset_weight,
|
||||
maint_liab_weight: perp_market.maint_liab_weight,
|
||||
base,
|
||||
quote,
|
||||
});
|
||||
retriever.perp_market(&account.fixed.group, i, perp_position.market_index)?;
|
||||
perp_infos.push(PerpInfo::new(perp_position, perp_market, &token_infos)?);
|
||||
}
|
||||
|
||||
Ok(HealthCache {
|
||||
|
@ -1064,7 +1159,7 @@ mod tests {
|
|||
// Run a health test that includes all the side values (like referrer_rebates_accrued)
|
||||
#[test]
|
||||
fn test_health0() {
|
||||
let buffer = MangoAccount::default().try_to_vec().unwrap();
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
|
||||
let group = Pubkey::new_unique();
|
||||
|
@ -1243,7 +1338,7 @@ mod tests {
|
|||
expected_health: f64,
|
||||
}
|
||||
fn test_health1_runner(testcase: &TestHealth1Case) {
|
||||
let buffer = MangoAccount::default().try_to_vec().unwrap();
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
|
||||
let group = Pubkey::new_unique();
|
||||
|
@ -1454,7 +1549,7 @@ mod tests {
|
|||
|
||||
for (i, testcase) in testcases.iter().enumerate() {
|
||||
println!("checking testcase {}", i);
|
||||
test_health1_runner(&testcase);
|
||||
test_health1_runner(testcase);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,11 +17,12 @@ use super::FillEvent;
|
|||
use super::LeafNode;
|
||||
use super::PerpMarket;
|
||||
use super::PerpMarketIndex;
|
||||
use super::PerpOpenOrders;
|
||||
use super::PerpOpenOrder;
|
||||
use super::Serum3MarketIndex;
|
||||
use super::Side;
|
||||
use super::TokenIndex;
|
||||
use super::FREE_ORDER_SLOT;
|
||||
use super::{HealthCache, HealthType};
|
||||
use super::{PerpPosition, Serum3Orders, TokenPosition};
|
||||
use checked_math as cm;
|
||||
|
||||
|
@ -56,9 +57,16 @@ pub struct MangoAccount {
|
|||
/// Normally accounts can not be liquidated while maint_health >= 0. But when an account
|
||||
/// reaches maint_health < 0, liquidators will call a liquidation instruction and thereby
|
||||
/// set this flag. Now the account may be liquidated until init_health >= 0.
|
||||
being_liquidated: u8,
|
||||
///
|
||||
/// Many actions should be disabled while the account is being liquidated, even if
|
||||
/// its maint health has recovered to positive. Creating new open orders would, for example,
|
||||
/// confuse liquidators.
|
||||
pub being_liquidated: u8,
|
||||
|
||||
padding2: u8,
|
||||
/// The account is currently inside a health region marked by HealthRegionBegin...HealthRegionEnd.
|
||||
///
|
||||
/// Must never be set after a transaction ends.
|
||||
pub in_health_region: u8,
|
||||
|
||||
pub bump: u8,
|
||||
|
||||
|
@ -72,7 +80,10 @@ pub struct MangoAccount {
|
|||
// TODO: unimplemented
|
||||
pub net_settled: i64,
|
||||
|
||||
pub reserved: [u8; 248],
|
||||
/// Init health as calculated during HealthReginBegin, rounded up.
|
||||
pub health_region_pre_init_health: i64,
|
||||
|
||||
pub reserved: [u8; 240],
|
||||
|
||||
// dynamic
|
||||
pub header_version: u8,
|
||||
|
@ -89,24 +100,25 @@ pub struct MangoAccount {
|
|||
pub padding6: u32,
|
||||
pub perps: Vec<PerpPosition>,
|
||||
pub padding7: u32,
|
||||
pub perp_open_orders: Vec<PerpOpenOrders>,
|
||||
pub perp_open_orders: Vec<PerpOpenOrder>,
|
||||
}
|
||||
|
||||
impl Default for MangoAccount {
|
||||
fn default() -> Self {
|
||||
impl MangoAccount {
|
||||
pub fn default_for_tests() -> Self {
|
||||
Self {
|
||||
name: Default::default(),
|
||||
group: Pubkey::default(),
|
||||
owner: Pubkey::default(),
|
||||
delegate: Pubkey::default(),
|
||||
being_liquidated: 0,
|
||||
padding2: 0,
|
||||
in_health_region: 0,
|
||||
account_num: 0,
|
||||
bump: 0,
|
||||
padding: Default::default(),
|
||||
net_deposits: 0,
|
||||
net_settled: 0,
|
||||
reserved: [0; 248],
|
||||
health_region_pre_init_health: 0,
|
||||
reserved: [0; 240],
|
||||
header_version: DEFAULT_MANGO_ACCOUNT_VERSION,
|
||||
padding3: Default::default(),
|
||||
padding4: Default::default(),
|
||||
|
@ -114,14 +126,13 @@ impl Default for MangoAccount {
|
|||
padding5: Default::default(),
|
||||
serum3: vec![Serum3Orders::default(); 5],
|
||||
padding6: Default::default(),
|
||||
perps: vec![PerpPosition::default(); 2],
|
||||
perps: vec![PerpPosition::default(); 4],
|
||||
padding7: Default::default(),
|
||||
perp_open_orders: vec![PerpOpenOrders::default(); 2],
|
||||
perp_open_orders: vec![PerpOpenOrder::default(); 6],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MangoAccount {
|
||||
/// Number of bytes needed for the MangoAccount, including the discriminator
|
||||
pub fn space(
|
||||
token_count: u8,
|
||||
serum3_count: u8,
|
||||
|
@ -167,59 +178,10 @@ impl MangoAccount {
|
|||
perp_oo_count: u8,
|
||||
) -> usize {
|
||||
Self::dynamic_perp_oo_vec_offset(token_count, serum3_count, perp_count)
|
||||
+ (BORSH_VEC_SIZE_BYTES + size_of::<PerpOpenOrders>() * usize::from(perp_oo_count))
|
||||
+ (BORSH_VEC_SIZE_BYTES + size_of::<PerpOpenOrder>() * usize::from(perp_oo_count))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization_match() {
|
||||
let mut account = MangoAccount::default();
|
||||
account.group = Pubkey::new_unique();
|
||||
account.owner = Pubkey::new_unique();
|
||||
account.name = crate::util::fill_from_str("abcdef").unwrap();
|
||||
account.delegate = Pubkey::new_unique();
|
||||
account.account_num = 1;
|
||||
account.bump = 2;
|
||||
account.net_deposits = 3;
|
||||
account.net_settled = 4;
|
||||
account.tokens.resize(8, TokenPosition::default());
|
||||
account.tokens[0].token_index = 5;
|
||||
account.serum3.resize(8, Serum3Orders::default());
|
||||
account.perps.resize(8, PerpPosition::default());
|
||||
account.perps[0].market_index = 6;
|
||||
account
|
||||
.perp_open_orders
|
||||
.resize(8, PerpOpenOrders::default());
|
||||
|
||||
let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap();
|
||||
assert_eq!(
|
||||
8 + account_bytes.len(),
|
||||
MangoAccount::space(8, 8, 8, 8).unwrap()
|
||||
);
|
||||
|
||||
let account2 = MangoAccountValue::from_bytes(&account_bytes).unwrap();
|
||||
assert_eq!(account.group, account2.fixed.group);
|
||||
assert_eq!(account.owner, account2.fixed.owner);
|
||||
assert_eq!(account.name, account2.fixed.name);
|
||||
assert_eq!(account.delegate, account2.fixed.delegate);
|
||||
assert_eq!(account.account_num, account2.fixed.account_num);
|
||||
assert_eq!(account.bump, account2.fixed.bump);
|
||||
assert_eq!(account.net_deposits, account2.fixed.net_deposits);
|
||||
assert_eq!(account.net_settled, account2.fixed.net_settled);
|
||||
assert_eq!(
|
||||
account.tokens[0].token_index,
|
||||
account2.token_position_by_raw_index(0).token_index
|
||||
);
|
||||
assert_eq!(
|
||||
account.serum3[0].open_orders,
|
||||
account2.serum3_orders_by_raw_index(0).open_orders
|
||||
);
|
||||
assert_eq!(
|
||||
account.perps[0].market_index,
|
||||
account2.perp_position_by_raw_index(0).market_index
|
||||
);
|
||||
}
|
||||
|
||||
// Mango Account fixed part for easy zero copy deserialization
|
||||
#[derive(Copy, Clone)]
|
||||
#[repr(C)]
|
||||
|
@ -230,14 +192,15 @@ pub struct MangoAccountFixed {
|
|||
pub delegate: Pubkey,
|
||||
pub account_num: u32,
|
||||
being_liquidated: u8,
|
||||
padding2: u8,
|
||||
in_health_region: u8,
|
||||
pub bump: u8,
|
||||
pub padding: [u8; 1],
|
||||
pub net_deposits: i64,
|
||||
pub net_settled: i64,
|
||||
pub reserved: [u8; 248],
|
||||
pub health_region_begin_init_health: i64,
|
||||
pub reserved: [u8; 240],
|
||||
}
|
||||
const_assert_eq!(size_of::<MangoAccountFixed>(), 32 * 4 + 8 + 2 * 8 + 248);
|
||||
const_assert_eq!(size_of::<MangoAccountFixed>(), 32 * 4 + 8 + 3 * 8 + 240);
|
||||
const_assert_eq!(size_of::<MangoAccountFixed>() % 8, 0);
|
||||
|
||||
unsafe impl bytemuck::Pod for MangoAccountFixed {}
|
||||
|
@ -255,18 +218,29 @@ impl MangoAccountFixed {
|
|||
}
|
||||
|
||||
pub fn being_liquidated(&self) -> bool {
|
||||
self.being_liquidated != 0
|
||||
self.being_liquidated == 1
|
||||
}
|
||||
|
||||
pub fn set_being_liquidated(&mut self, b: bool) {
|
||||
self.being_liquidated = if b { 1 } else { 0 };
|
||||
}
|
||||
|
||||
pub fn maybe_recover_from_being_liquidated(&mut self, init_health: I80F48) {
|
||||
pub fn is_in_health_region(&self) -> bool {
|
||||
self.in_health_region == 1
|
||||
}
|
||||
|
||||
pub fn set_in_health_region(&mut self, b: bool) {
|
||||
self.in_health_region = if b { 1 } else { 0 };
|
||||
}
|
||||
|
||||
pub fn maybe_recover_from_being_liquidated(&mut self, init_health: I80F48) -> bool {
|
||||
// This is used as threshold to flip flag instead of 0 because of dust issues
|
||||
let one_native_usdc = I80F48::ONE;
|
||||
if self.being_liquidated() && init_health > -one_native_usdc {
|
||||
self.set_being_liquidated(false);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -374,7 +348,7 @@ impl MangoAccountDynamicHeader {
|
|||
self.serum3_count,
|
||||
self.perp_count,
|
||||
) + BORSH_VEC_SIZE_BYTES
|
||||
+ raw_index * size_of::<PerpOpenOrders>()
|
||||
+ raw_index * size_of::<PerpOpenOrder>()
|
||||
}
|
||||
|
||||
pub fn token_count(&self) -> usize {
|
||||
|
@ -474,14 +448,13 @@ impl<
|
|||
|
||||
// get iter over all active TokenPositions
|
||||
pub fn active_token_positions(&self) -> impl Iterator<Item = &TokenPosition> + '_ {
|
||||
(0..self.header().token_count())
|
||||
.map(|i| self.token_position_by_raw_index(i))
|
||||
.filter(|token| token.is_active())
|
||||
self.all_token_positions().filter(|token| token.is_active())
|
||||
}
|
||||
|
||||
pub fn serum3_orders(&self, market_index: Serum3MarketIndex) -> Option<&Serum3Orders> {
|
||||
self.active_serum3_orders()
|
||||
pub fn serum3_orders(&self, market_index: Serum3MarketIndex) -> Result<&Serum3Orders> {
|
||||
self.all_serum3_orders()
|
||||
.find(|p| p.is_active_for_market(market_index))
|
||||
.ok_or_else(|| error_msg!("serum3 orders for market index {} not found", market_index))
|
||||
}
|
||||
|
||||
pub fn serum3_orders_by_raw_index(&self, raw_index: usize) -> &Serum3Orders {
|
||||
|
@ -493,14 +466,14 @@ impl<
|
|||
}
|
||||
|
||||
pub fn active_serum3_orders(&self) -> impl Iterator<Item = &Serum3Orders> + '_ {
|
||||
(0..self.header().serum3_count())
|
||||
.map(|i| self.serum3_orders_by_raw_index(i))
|
||||
self.all_serum3_orders()
|
||||
.filter(|serum3_order| serum3_order.is_active())
|
||||
}
|
||||
|
||||
pub fn perp_position(&self, market_index: PerpMarketIndex) -> Option<&PerpPosition> {
|
||||
self.active_perp_positions()
|
||||
pub fn perp_position(&self, market_index: PerpMarketIndex) -> Result<&PerpPosition> {
|
||||
self.all_perp_positions()
|
||||
.find(|p| p.is_active_for_market(market_index))
|
||||
.ok_or_else(|| error_msg!("perp position for market index {} not found", market_index))
|
||||
}
|
||||
|
||||
pub fn perp_position_by_raw_index(&self, raw_index: usize) -> &PerpPosition {
|
||||
|
@ -512,22 +485,21 @@ impl<
|
|||
}
|
||||
|
||||
pub fn active_perp_positions(&self) -> impl Iterator<Item = &PerpPosition> {
|
||||
(0..self.header().perp_count())
|
||||
.map(|i| self.perp_position_by_raw_index(i))
|
||||
.filter(|p| p.is_active())
|
||||
self.all_perp_positions().filter(|p| p.is_active())
|
||||
}
|
||||
|
||||
pub fn perp_orders_by_raw_index(&self, raw_index: usize) -> &PerpOpenOrders {
|
||||
pub fn perp_order_by_raw_index(&self, raw_index: usize) -> &PerpOpenOrder {
|
||||
get_helper(self.dynamic(), self.header().perp_oo_offset(raw_index))
|
||||
}
|
||||
|
||||
pub fn all_perp_orders(&self) -> impl Iterator<Item = &PerpOpenOrders> {
|
||||
(0..self.header().perp_oo_count()).map(|i| self.perp_orders_by_raw_index(i))
|
||||
pub fn all_perp_orders(&self) -> impl Iterator<Item = &PerpOpenOrder> {
|
||||
(0..self.header().perp_oo_count()).map(|i| self.perp_order_by_raw_index(i))
|
||||
}
|
||||
|
||||
pub fn perp_next_order_slot(&self) -> Option<usize> {
|
||||
pub fn perp_next_order_slot(&self) -> Result<usize> {
|
||||
self.all_perp_orders()
|
||||
.position(|&oo| oo.order_market == FREE_ORDER_SLOT)
|
||||
.ok_or_else(|| error_msg!("no free perp order index"))
|
||||
}
|
||||
|
||||
pub fn perp_find_order_with_client_order_id(
|
||||
|
@ -535,8 +507,7 @@ impl<
|
|||
market_index: PerpMarketIndex,
|
||||
client_order_id: u64,
|
||||
) -> Option<(i128, Side)> {
|
||||
for i in 0..self.header().perp_oo_count() {
|
||||
let oo = self.perp_orders_by_raw_index(i);
|
||||
for oo in self.all_perp_orders() {
|
||||
if oo.order_market == market_index && oo.client_order_id == client_order_id {
|
||||
return Some((oo.order_id, oo.order_side));
|
||||
}
|
||||
|
@ -549,8 +520,7 @@ impl<
|
|||
market_index: PerpMarketIndex,
|
||||
order_id: i128,
|
||||
) -> Option<Side> {
|
||||
for i in 0..self.header().perp_oo_count() {
|
||||
let oo = self.perp_orders_by_raw_index(i);
|
||||
for oo in self.all_perp_orders() {
|
||||
if oo.order_market == market_index && oo.order_id == order_id {
|
||||
return Some(oo.order_side);
|
||||
}
|
||||
|
@ -580,6 +550,9 @@ impl<
|
|||
fn header_mut(&mut self) -> &mut MangoAccountDynamicHeader {
|
||||
self.header.deref_or_borrow_mut()
|
||||
}
|
||||
fn fixed_mut(&mut self) -> &mut MangoAccountFixed {
|
||||
self.fixed.deref_or_borrow_mut()
|
||||
}
|
||||
fn dynamic_mut(&mut self) -> &mut [u8] {
|
||||
self.dynamic.deref_or_borrow_mut()
|
||||
}
|
||||
|
@ -669,7 +642,7 @@ impl<
|
|||
&mut self,
|
||||
market_index: Serum3MarketIndex,
|
||||
) -> Result<&mut Serum3Orders> {
|
||||
if self.serum3_orders(market_index).is_some() {
|
||||
if self.serum3_orders(market_index).is_ok() {
|
||||
return err!(MangoError::Serum3OpenOrdersExistAlready);
|
||||
}
|
||||
|
||||
|
@ -679,9 +652,9 @@ impl<
|
|||
market_index: market_index as Serum3MarketIndex,
|
||||
..Serum3Orders::default()
|
||||
};
|
||||
return Ok(self.serum3_orders_mut_by_raw_index(raw_index));
|
||||
Ok(self.serum3_orders_mut_by_raw_index(raw_index))
|
||||
} else {
|
||||
return err!(MangoError::NoFreeSerum3OpenOrdersIndex);
|
||||
err!(MangoError::NoFreeSerum3OpenOrdersIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -697,11 +670,13 @@ impl<
|
|||
pub fn serum3_orders_mut(
|
||||
&mut self,
|
||||
market_index: Serum3MarketIndex,
|
||||
) -> Option<&mut Serum3Orders> {
|
||||
) -> Result<&mut Serum3Orders> {
|
||||
let raw_index_opt = self
|
||||
.active_serum3_orders()
|
||||
.all_serum3_orders()
|
||||
.position(|p| p.is_active_for_market(market_index));
|
||||
raw_index_opt.map(|raw_index| self.serum3_orders_mut_by_raw_index(raw_index))
|
||||
raw_index_opt
|
||||
.map(|raw_index| self.serum3_orders_mut_by_raw_index(raw_index))
|
||||
.ok_or_else(|| error_msg!("serum3 orders for market index {} not found", market_index))
|
||||
}
|
||||
|
||||
// get mut PerpPosition at raw_index
|
||||
|
@ -710,7 +685,7 @@ impl<
|
|||
get_helper_mut(self.dynamic_mut(), offset)
|
||||
}
|
||||
|
||||
pub fn perp_orders_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpOpenOrders {
|
||||
pub fn perp_order_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpOpenOrder {
|
||||
let offset = self.header().perp_oo_offset(raw_index);
|
||||
get_helper_mut(self.dynamic_mut(), offset)
|
||||
}
|
||||
|
@ -720,7 +695,7 @@ impl<
|
|||
perp_market_index: PerpMarketIndex,
|
||||
) -> Result<(&mut PerpPosition, usize)> {
|
||||
let mut raw_index_opt = self
|
||||
.active_perp_positions()
|
||||
.all_perp_positions()
|
||||
.position(|p| p.is_active_for_market(perp_market_index));
|
||||
if raw_index_opt.is_none() {
|
||||
raw_index_opt = self.all_perp_positions().position(|p| !p.is_active());
|
||||
|
@ -748,6 +723,7 @@ impl<
|
|||
side: Side,
|
||||
order: &LeafNode,
|
||||
) -> Result<()> {
|
||||
// TODO: pass in the PerpPosition, currently has a creation side-effect
|
||||
let mut perp_account = self.ensure_perp_position(perp_market_index).unwrap().0;
|
||||
match side {
|
||||
Side::Bid => {
|
||||
|
@ -759,7 +735,7 @@ impl<
|
|||
};
|
||||
let slot = order.owner_slot as usize;
|
||||
|
||||
let mut oo = self.perp_orders_mut_by_raw_index(slot);
|
||||
let mut oo = self.perp_order_mut_by_raw_index(slot);
|
||||
oo.order_market = perp_market_index;
|
||||
oo.order_side = side;
|
||||
oo.order_id = order.key;
|
||||
|
@ -768,8 +744,9 @@ impl<
|
|||
}
|
||||
|
||||
pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
|
||||
// TODO: pass in the PerpPosition, currently has a creation side-effect
|
||||
{
|
||||
let oo = self.perp_orders_mut_by_raw_index(slot);
|
||||
let oo = self.perp_order_mut_by_raw_index(slot);
|
||||
require_neq!(oo.order_market, FREE_ORDER_SLOT);
|
||||
let order_side = oo.order_side;
|
||||
let perp_market_index = oo.order_market;
|
||||
|
@ -787,7 +764,7 @@ impl<
|
|||
}
|
||||
|
||||
// release space
|
||||
let oo = self.perp_orders_mut_by_raw_index(slot);
|
||||
let oo = self.perp_order_mut_by_raw_index(slot);
|
||||
oo.order_market = FREE_ORDER_SLOT;
|
||||
oo.order_side = Side::Bid;
|
||||
oo.order_id = 0i128;
|
||||
|
@ -801,6 +778,7 @@ impl<
|
|||
perp_market: &mut PerpMarket,
|
||||
fill: &FillEvent,
|
||||
) -> Result<()> {
|
||||
// TODO: pass in the PerpPosition, currently has a creation side-effect
|
||||
let pa = self.ensure_perp_position(perp_market_index).unwrap().0;
|
||||
pa.settle_funding(perp_market);
|
||||
|
||||
|
@ -840,6 +818,7 @@ impl<
|
|||
perp_market: &mut PerpMarket,
|
||||
fill: &FillEvent,
|
||||
) -> Result<()> {
|
||||
// TODO: pass in the PerpPosition, currently has a creation side-effect
|
||||
let pa = self.ensure_perp_position(perp_market_index).unwrap().0;
|
||||
pa.settle_funding(perp_market);
|
||||
|
||||
|
@ -854,6 +833,34 @@ impl<
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_health_pre(&mut self, health_cache: &HealthCache) -> Result<I80F48> {
|
||||
let pre_health = health_cache.health(HealthType::Init);
|
||||
msg!("pre_health: {}", pre_health);
|
||||
self.fixed_mut()
|
||||
.maybe_recover_from_being_liquidated(pre_health);
|
||||
require!(
|
||||
!self.fixed().being_liquidated(),
|
||||
MangoError::BeingLiquidated
|
||||
);
|
||||
Ok(pre_health)
|
||||
}
|
||||
|
||||
pub fn check_health_post(
|
||||
&mut self,
|
||||
health_cache: &HealthCache,
|
||||
pre_health: I80F48,
|
||||
) -> Result<()> {
|
||||
let post_health = health_cache.health(HealthType::Init);
|
||||
msg!("post_health: {}", post_health);
|
||||
require!(
|
||||
post_health >= 0 || post_health > pre_health,
|
||||
MangoError::HealthMustBePositiveOrIncrease
|
||||
);
|
||||
self.fixed_mut()
|
||||
.maybe_recover_from_being_liquidated(post_health);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// writes length of tokens vec at appropriate offset so that borsh can infer the vector length
|
||||
// length used is that present in the header
|
||||
fn write_token_length(&mut self) {
|
||||
|
@ -934,12 +941,12 @@ impl<
|
|||
sol_memmove(
|
||||
&mut dynamic[new_header.perp_oo_offset(0)],
|
||||
&mut dynamic[old_header.perp_oo_offset(0)],
|
||||
size_of::<PerpOpenOrders>() * old_header.perp_oo_count(),
|
||||
size_of::<PerpOpenOrder>() * 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())) =
|
||||
PerpOpenOrders::default();
|
||||
PerpOpenOrder::default();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1000,3 +1007,237 @@ impl<
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_test_account() -> MangoAccountValue {
|
||||
let bytes = AnchorSerialize::try_to_vec(&MangoAccount::default_for_tests()).unwrap();
|
||||
MangoAccountValue::from_bytes(&bytes).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization_match() {
|
||||
let mut account = MangoAccount::default_for_tests();
|
||||
account.group = Pubkey::new_unique();
|
||||
account.owner = Pubkey::new_unique();
|
||||
account.name = crate::util::fill_from_str("abcdef").unwrap();
|
||||
account.delegate = Pubkey::new_unique();
|
||||
account.account_num = 1;
|
||||
account.being_liquidated = 2;
|
||||
account.in_health_region = 3;
|
||||
account.bump = 4;
|
||||
account.net_deposits = 5;
|
||||
account.net_settled = 6;
|
||||
account.health_region_pre_init_health = 7;
|
||||
account.tokens.resize(8, TokenPosition::default());
|
||||
account.tokens[0].token_index = 8;
|
||||
account.serum3.resize(8, Serum3Orders::default());
|
||||
account.perps.resize(8, PerpPosition::default());
|
||||
account.perps[0].market_index = 9;
|
||||
account.perp_open_orders.resize(8, PerpOpenOrder::default());
|
||||
|
||||
let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap();
|
||||
assert_eq!(
|
||||
8 + account_bytes.len(),
|
||||
MangoAccount::space(8, 8, 8, 8).unwrap()
|
||||
);
|
||||
|
||||
let account2 = MangoAccountValue::from_bytes(&account_bytes).unwrap();
|
||||
assert_eq!(account.group, account2.fixed.group);
|
||||
assert_eq!(account.owner, account2.fixed.owner);
|
||||
assert_eq!(account.name, account2.fixed.name);
|
||||
assert_eq!(account.delegate, account2.fixed.delegate);
|
||||
assert_eq!(account.account_num, account2.fixed.account_num);
|
||||
assert_eq!(account.being_liquidated, account2.fixed.being_liquidated);
|
||||
assert_eq!(account.in_health_region, account2.fixed.in_health_region);
|
||||
assert_eq!(account.bump, account2.fixed.bump);
|
||||
assert_eq!(account.net_deposits, account2.fixed.net_deposits);
|
||||
assert_eq!(account.net_settled, account2.fixed.net_settled);
|
||||
assert_eq!(
|
||||
account.health_region_pre_init_health,
|
||||
account2.fixed.health_region_begin_init_health
|
||||
);
|
||||
assert_eq!(
|
||||
account.tokens[0].token_index,
|
||||
account2.token_position_by_raw_index(0).token_index
|
||||
);
|
||||
assert_eq!(
|
||||
account.serum3[0].open_orders,
|
||||
account2.serum3_orders_by_raw_index(0).open_orders
|
||||
);
|
||||
assert_eq!(
|
||||
account.perps[0].market_index,
|
||||
account2.perp_position_by_raw_index(0).market_index
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_positions() {
|
||||
let mut account = make_test_account();
|
||||
assert!(account.token_position(1).is_err());
|
||||
assert!(account.token_position_and_raw_index(2).is_err());
|
||||
assert!(account.token_position_mut(3).is_err());
|
||||
assert_eq!(
|
||||
account.token_position_by_raw_index(0).token_index,
|
||||
TokenIndex::MAX
|
||||
);
|
||||
|
||||
{
|
||||
let (pos, raw, active) = account.ensure_token_position(1).unwrap();
|
||||
assert_eq!(raw, 0);
|
||||
assert_eq!(active, 0);
|
||||
assert_eq!(pos.token_index, 1);
|
||||
}
|
||||
{
|
||||
let (pos, raw, active) = account.ensure_token_position(7).unwrap();
|
||||
assert_eq!(raw, 1);
|
||||
assert_eq!(active, 1);
|
||||
assert_eq!(pos.token_index, 7);
|
||||
}
|
||||
{
|
||||
let (pos, raw, active) = account.ensure_token_position(42).unwrap();
|
||||
assert_eq!(raw, 2);
|
||||
assert_eq!(active, 2);
|
||||
assert_eq!(pos.token_index, 42);
|
||||
}
|
||||
|
||||
{
|
||||
account.deactivate_token_position(1);
|
||||
|
||||
let (pos, raw, active) = account.ensure_token_position(42).unwrap();
|
||||
assert_eq!(raw, 2);
|
||||
assert_eq!(active, 1);
|
||||
assert_eq!(pos.token_index, 42);
|
||||
|
||||
let (pos, raw, active) = account.ensure_token_position(8).unwrap();
|
||||
assert_eq!(raw, 1);
|
||||
assert_eq!(active, 1);
|
||||
assert_eq!(pos.token_index, 8);
|
||||
}
|
||||
|
||||
assert_eq!(account.active_token_positions().count(), 3);
|
||||
account.deactivate_token_position(0);
|
||||
assert_eq!(
|
||||
account.token_position_by_raw_index(0).token_index,
|
||||
TokenIndex::MAX
|
||||
);
|
||||
assert!(account.token_position(1).is_err());
|
||||
assert!(account.token_position_mut(1).is_err());
|
||||
assert!(account.token_position(8).is_ok());
|
||||
assert!(account.token_position(42).is_ok());
|
||||
assert_eq!(account.token_position_and_raw_index(42).unwrap().1, 2);
|
||||
assert_eq!(account.active_token_positions().count(), 2);
|
||||
|
||||
{
|
||||
let (pos, raw) = account.token_position_mut(42).unwrap();
|
||||
assert_eq!(pos.token_index, 42);
|
||||
assert_eq!(raw, 2);
|
||||
}
|
||||
{
|
||||
let (pos, raw) = account.token_position_mut(8).unwrap();
|
||||
assert_eq!(pos.token_index, 8);
|
||||
assert_eq!(raw, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serum3_orders() {
|
||||
let mut account = make_test_account();
|
||||
assert!(account.serum3_orders(1).is_err());
|
||||
assert!(account.serum3_orders_mut(3).is_err());
|
||||
assert_eq!(
|
||||
account.serum3_orders_by_raw_index(0).market_index,
|
||||
Serum3MarketIndex::MAX
|
||||
);
|
||||
|
||||
assert_eq!(account.create_serum3_orders(1).unwrap().market_index, 1);
|
||||
assert_eq!(account.create_serum3_orders(7).unwrap().market_index, 7);
|
||||
assert_eq!(account.create_serum3_orders(42).unwrap().market_index, 42);
|
||||
assert!(account.create_serum3_orders(7).is_err());
|
||||
assert_eq!(account.active_serum3_orders().count(), 3);
|
||||
|
||||
assert!(account.deactivate_serum3_orders(7).is_ok());
|
||||
assert_eq!(
|
||||
account.serum3_orders_by_raw_index(1).market_index,
|
||||
Serum3MarketIndex::MAX
|
||||
);
|
||||
assert!(account.create_serum3_orders(8).is_ok());
|
||||
assert_eq!(account.serum3_orders_by_raw_index(1).market_index, 8);
|
||||
|
||||
assert_eq!(account.active_serum3_orders().count(), 3);
|
||||
assert!(account.deactivate_serum3_orders(1).is_ok());
|
||||
assert!(account.serum3_orders(1).is_err());
|
||||
assert!(account.serum3_orders_mut(1).is_err());
|
||||
assert!(account.serum3_orders(8).is_ok());
|
||||
assert!(account.serum3_orders(42).is_ok());
|
||||
assert_eq!(account.active_serum3_orders().count(), 2);
|
||||
|
||||
assert_eq!(account.serum3_orders_mut(42).unwrap().market_index, 42);
|
||||
assert_eq!(account.serum3_orders_mut(8).unwrap().market_index, 8);
|
||||
assert!(account.serum3_orders_mut(7).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_perp_positions() {
|
||||
let mut account = make_test_account();
|
||||
assert!(account.perp_position(1).is_err());
|
||||
//assert!(account.perp_position_mut(3).is_err());
|
||||
assert_eq!(
|
||||
account.perp_position_by_raw_index(0).market_index,
|
||||
PerpMarketIndex::MAX
|
||||
);
|
||||
|
||||
{
|
||||
let (pos, raw) = account.ensure_perp_position(1).unwrap();
|
||||
assert_eq!(raw, 0);
|
||||
assert_eq!(pos.market_index, 1);
|
||||
}
|
||||
{
|
||||
let (pos, raw) = account.ensure_perp_position(7).unwrap();
|
||||
assert_eq!(raw, 1);
|
||||
assert_eq!(pos.market_index, 7);
|
||||
}
|
||||
{
|
||||
let (pos, raw) = account.ensure_perp_position(42).unwrap();
|
||||
assert_eq!(raw, 2);
|
||||
assert_eq!(pos.market_index, 42);
|
||||
}
|
||||
|
||||
{
|
||||
account.deactivate_perp_position(1);
|
||||
|
||||
let (pos, raw) = account.ensure_perp_position(42).unwrap();
|
||||
assert_eq!(raw, 2);
|
||||
assert_eq!(pos.market_index, 42);
|
||||
|
||||
let (pos, raw) = account.ensure_perp_position(8).unwrap();
|
||||
assert_eq!(raw, 1);
|
||||
assert_eq!(pos.market_index, 8);
|
||||
}
|
||||
|
||||
assert_eq!(account.active_perp_positions().count(), 3);
|
||||
account.deactivate_perp_position(0);
|
||||
assert_eq!(
|
||||
account.perp_position_by_raw_index(0).market_index,
|
||||
PerpMarketIndex::MAX
|
||||
);
|
||||
assert!(account.perp_position(1).is_err());
|
||||
//assert!(account.perp_position_mut(1).is_err());
|
||||
assert!(account.perp_position(8).is_ok());
|
||||
assert!(account.perp_position(42).is_ok());
|
||||
assert_eq!(account.active_perp_positions().count(), 2);
|
||||
|
||||
/*{
|
||||
let (pos, raw) = account.perp_position_mut(42).unwrap();
|
||||
assert_eq!(pos.perp_index, 42);
|
||||
assert_eq!(raw, 2);
|
||||
}
|
||||
{
|
||||
let (pos, raw) = account.perp_position_mut(8).unwrap();
|
||||
assert_eq!(pos.perp_index, 8);
|
||||
assert_eq!(raw, 1);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,12 +92,12 @@ impl TokenPosition {
|
|||
pub struct Serum3Orders {
|
||||
pub open_orders: Pubkey,
|
||||
|
||||
// tracks reserved funds in open orders account,
|
||||
// used for bookkeeping of potentital loans which
|
||||
// can be charged with loan origination fees
|
||||
// e.g. serum3 settle funds ix
|
||||
pub previous_native_coin_reserved: u64,
|
||||
pub previous_native_pc_reserved: u64,
|
||||
/// Tracks the amount of borrows that have flowed into the serum open orders account.
|
||||
/// These borrows did not have the loan origination fee applied, and that may happen
|
||||
/// later (in serum3_settle_funds) if we can guarantee that the funds were used.
|
||||
/// In particular a place-on-book, cancel, settle should not cost fees.
|
||||
pub base_borrows_without_fee: u64,
|
||||
pub quote_borrows_without_fee: u64,
|
||||
|
||||
pub market_index: Serum3MarketIndex,
|
||||
|
||||
|
@ -138,8 +138,8 @@ impl Default for Serum3Orders {
|
|||
quote_token_index: TokenIndex::MAX,
|
||||
reserved: [0; 64],
|
||||
padding: Default::default(),
|
||||
previous_native_coin_reserved: 0,
|
||||
previous_native_pc_reserved: 0,
|
||||
base_borrows_without_fee: 0,
|
||||
quote_borrows_without_fee: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -321,7 +321,7 @@ impl PerpPosition {
|
|||
|
||||
#[zero_copy]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
|
||||
pub struct PerpOpenOrders {
|
||||
pub struct PerpOpenOrder {
|
||||
pub order_side: Side, // TODO: storing enums isn't POD
|
||||
pub padding1: [u8; 1],
|
||||
pub order_market: PerpMarketIndex,
|
||||
|
@ -331,7 +331,7 @@ pub struct PerpOpenOrders {
|
|||
pub reserved: [u8; 64],
|
||||
}
|
||||
|
||||
impl Default for PerpOpenOrders {
|
||||
impl Default for PerpOpenOrder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
order_side: Side::Bid,
|
||||
|
@ -345,11 +345,11 @@ impl Default for PerpOpenOrders {
|
|||
}
|
||||
}
|
||||
|
||||
unsafe impl bytemuck::Pod for PerpOpenOrders {}
|
||||
unsafe impl bytemuck::Zeroable for PerpOpenOrders {}
|
||||
unsafe impl bytemuck::Pod for PerpOpenOrder {}
|
||||
unsafe impl bytemuck::Zeroable for PerpOpenOrder {}
|
||||
|
||||
const_assert_eq!(size_of::<PerpOpenOrders>(), 1 + 1 + 2 + 4 + 8 + 16 + 64);
|
||||
const_assert_eq!(size_of::<PerpOpenOrders>() % 8, 0);
|
||||
const_assert_eq!(size_of::<PerpOpenOrder>(), 1 + 1 + 2 + 4 + 8 + 16 + 64);
|
||||
const_assert_eq!(size_of::<PerpOpenOrder>() % 8, 0);
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! account_seeds {
|
||||
|
|
|
@ -346,9 +346,7 @@ impl<'a> Book<'a> {
|
|||
event_queue.push_back(cast(event)).unwrap();
|
||||
}
|
||||
|
||||
let owner_slot = mango_account
|
||||
.perp_next_order_slot()
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?;
|
||||
let owner_slot = mango_account.perp_next_order_slot()?;
|
||||
let new_order = LeafNode::new(
|
||||
owner_slot as u8,
|
||||
order_id,
|
||||
|
@ -393,7 +391,7 @@ impl<'a> Book<'a> {
|
|||
side_to_cancel_option: Option<Side>,
|
||||
) -> Result<()> {
|
||||
for i in 0..mango_account.header.perp_oo_count() {
|
||||
let oo = mango_account.perp_orders_by_raw_index(i);
|
||||
let oo = mango_account.perp_order_by_raw_index(i);
|
||||
if oo.order_market == FREE_ORDER_SLOT
|
||||
|| oo.order_market != perp_market.perp_market_index
|
||||
{
|
||||
|
|
|
@ -101,7 +101,7 @@ mod tests {
|
|||
|
||||
let mut new_order =
|
||||
|book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 {
|
||||
let buffer = MangoAccount::default().try_to_vec().unwrap();
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
|
||||
let quantity = 1;
|
||||
|
@ -124,7 +124,7 @@ mod tests {
|
|||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
account.perp_orders_by_raw_index(0).order_id
|
||||
account.perp_order_by_raw_index(0).order_id
|
||||
};
|
||||
|
||||
// insert bids until book side is full
|
||||
|
@ -196,7 +196,7 @@ mod tests {
|
|||
market.maker_fee = I80F48::from_num(-0.001f64);
|
||||
market.taker_fee = I80F48::from_num(0.01f64);
|
||||
|
||||
let buffer = MangoAccount::default().try_to_vec().unwrap();
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut maker = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
let mut taker = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
|
||||
|
@ -225,19 +225,19 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
maker.perp_orders_mut_by_raw_index(0).order_market,
|
||||
maker.perp_order_mut_by_raw_index(0).order_market,
|
||||
market.perp_market_index
|
||||
);
|
||||
assert_eq!(
|
||||
maker.perp_orders_mut_by_raw_index(1).order_market,
|
||||
maker.perp_order_mut_by_raw_index(1).order_market,
|
||||
FREE_ORDER_SLOT
|
||||
);
|
||||
assert_ne!(maker.perp_orders_mut_by_raw_index(0).order_id, 0);
|
||||
assert_eq!(maker.perp_orders_mut_by_raw_index(0).client_order_id, 42);
|
||||
assert_eq!(maker.perp_orders_mut_by_raw_index(0).order_side, Side::Bid);
|
||||
assert_ne!(maker.perp_order_mut_by_raw_index(0).order_id, 0);
|
||||
assert_eq!(maker.perp_order_mut_by_raw_index(0).client_order_id, 42);
|
||||
assert_eq!(maker.perp_order_mut_by_raw_index(0).order_side, Side::Bid);
|
||||
assert!(bookside_contains_key(
|
||||
&book.bids,
|
||||
maker.perp_orders_mut_by_raw_index(0).order_id
|
||||
maker.perp_order_mut_by_raw_index(0).order_id
|
||||
));
|
||||
assert!(bookside_contains_price(&book.bids, price));
|
||||
assert_eq!(
|
||||
|
@ -279,7 +279,7 @@ mod tests {
|
|||
// the remainder of the maker order is still on the book
|
||||
// (the maker account is unchanged: it was not even passed in)
|
||||
let order =
|
||||
bookside_leaf_by_key(&book.bids, maker.perp_orders_by_raw_index(0).order_id).unwrap();
|
||||
bookside_leaf_by_key(&book.bids, maker.perp_order_by_raw_index(0).order_id).unwrap();
|
||||
assert_eq!(order.price(), price);
|
||||
assert_eq!(order.quantity, bid_quantity - match_quantity);
|
||||
|
||||
|
@ -292,7 +292,7 @@ mod tests {
|
|||
|
||||
// the taker account is updated
|
||||
assert_eq!(
|
||||
taker.perp_orders_by_raw_index(0).order_market,
|
||||
taker.perp_order_by_raw_index(0).order_market,
|
||||
FREE_ORDER_SLOT
|
||||
);
|
||||
assert_eq!(taker.perp_position_by_raw_index(0).bids_base_lots, 0);
|
||||
|
@ -327,14 +327,14 @@ mod tests {
|
|||
|
||||
// simulate event queue processing
|
||||
maker
|
||||
.execute_perp_maker(market.perp_market_index, &mut market, &fill)
|
||||
.execute_perp_maker(market.perp_market_index, &mut market, fill)
|
||||
.unwrap();
|
||||
taker
|
||||
.execute_perp_taker(market.perp_market_index, &mut market, &fill)
|
||||
.execute_perp_taker(market.perp_market_index, &mut market, fill)
|
||||
.unwrap();
|
||||
assert_eq!(market.open_interest, 2 * match_quantity);
|
||||
|
||||
assert_eq!(maker.perp_orders_by_raw_index(0).order_market, 0);
|
||||
assert_eq!(maker.perp_order_by_raw_index(0).order_market, 0);
|
||||
assert_eq!(
|
||||
maker.perp_position_by_raw_index(0).bids_base_lots,
|
||||
bid_quantity - match_quantity
|
||||
|
|
|
@ -37,5 +37,4 @@ pub fn format_zero_terminated_utf8_bytes(
|
|||
.unwrap()
|
||||
.trim_matches(char::from(0)),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -319,6 +319,28 @@ pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank:
|
|||
native.to_num::<f64>()
|
||||
}
|
||||
|
||||
// Verifies that the "post_health: ..." log emitted by the previous instruction
|
||||
// matches the init health of the account.
|
||||
pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account: Pubkey) {
|
||||
let logs = solana.program_log();
|
||||
let post_health_str = logs
|
||||
.iter()
|
||||
.find_map(|line| line.strip_prefix("post_health: "))
|
||||
.unwrap();
|
||||
let post_health = post_health_str.parse::<f64>().unwrap();
|
||||
|
||||
solana.advance_by_slots(1).await; // ugly, just to avoid sending the same tx next
|
||||
send_tx(solana, ComputeAccountDataInstruction { account })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let health_data = solana
|
||||
.program_log_events::<mango_v4::events::MangoAccountData>()
|
||||
.pop()
|
||||
.unwrap();
|
||||
assert_eq!(health_data.init_health.to_num::<f64>(), post_health);
|
||||
}
|
||||
|
||||
//
|
||||
// a struct for each instruction along with its
|
||||
// ClientInstruction impl
|
||||
|
@ -502,6 +524,7 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> {
|
|||
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_program: Token::id(),
|
||||
};
|
||||
|
@ -569,6 +592,7 @@ impl ClientInstruction for TokenDepositInstruction {
|
|||
account: self.account,
|
||||
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(),
|
||||
|
@ -812,9 +836,7 @@ impl<'keypair> ClientInstruction for TokenDeregisterInstruction<'keypair> {
|
|||
_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {
|
||||
token_index: self.token_index,
|
||||
};
|
||||
let instruction = Self::Instruction {};
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
admin: self.admin.pubkey(),
|
||||
|
@ -1321,12 +1343,23 @@ impl<'keypair> ClientInstruction for Serum3RegisterMarketInstruction<'keypair> {
|
|||
)
|
||||
.0;
|
||||
|
||||
let index_reservation = Pubkey::find_program_address(
|
||||
&[
|
||||
b"Serum3Index".as_ref(),
|
||||
self.group.as_ref(),
|
||||
&self.market_index.to_le_bytes(),
|
||||
],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
admin: self.admin.pubkey(),
|
||||
serum_program: self.serum_program,
|
||||
serum_market_external: self.serum_market_external,
|
||||
serum_market,
|
||||
index_reservation,
|
||||
base_bank: self.base_bank,
|
||||
quote_bank: self.quote_bank,
|
||||
payer: self.payer.pubkey(),
|
||||
|
@ -1567,14 +1600,17 @@ impl<'keypair> ClientInstruction for Serum3PlaceOrderInstruction<'keypair> {
|
|||
)
|
||||
.await;
|
||||
|
||||
let (payer_bank, payer_vault) = match self.side {
|
||||
Serum3Side::Bid => (quote_info.first_bank(), quote_info.first_vault()),
|
||||
Serum3Side::Ask => (base_info.first_bank(), base_info.first_vault()),
|
||||
};
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: account.fixed.group,
|
||||
account: self.account,
|
||||
open_orders,
|
||||
quote_bank: quote_info.first_bank(),
|
||||
quote_vault: quote_info.first_vault(),
|
||||
base_bank: base_info.first_bank(),
|
||||
base_vault: base_info.first_vault(),
|
||||
payer_bank,
|
||||
payer_vault,
|
||||
serum_market: self.serum_market,
|
||||
serum_program: serum_market.serum_program,
|
||||
serum_market_external: serum_market.serum_market_external,
|
||||
|
@ -2499,7 +2535,6 @@ impl ClientInstruction for TokenUpdateIndexAndRateInstruction {
|
|||
|
||||
pub struct ComputeAccountDataInstruction {
|
||||
pub account: Pubkey,
|
||||
pub health_type: HealthType,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for ComputeAccountDataInstruction {
|
||||
|
@ -2541,3 +2576,91 @@ impl ClientInstruction for ComputeAccountDataInstruction {
|
|||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HealthRegionBeginInstruction {
|
||||
pub account: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for HealthRegionBeginInstruction {
|
||||
type Accounts = mango_v4::accounts::HealthRegionBegin;
|
||||
type Instruction = mango_v4::instruction::HealthRegionBegin;
|
||||
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 {};
|
||||
|
||||
let account = account_loader
|
||||
.load_mango_account(&self.account)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let health_check_metas = derive_health_check_remaining_account_metas(
|
||||
&account_loader,
|
||||
&account,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
instructions: solana_program::sysvar::instructions::id(),
|
||||
account: self.account,
|
||||
};
|
||||
|
||||
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||
instruction.accounts.extend(health_check_metas.into_iter());
|
||||
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<&Keypair> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HealthRegionEndInstruction {
|
||||
pub account: Pubkey,
|
||||
pub affected_bank: Option<Pubkey>,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for HealthRegionEndInstruction {
|
||||
type Accounts = mango_v4::accounts::HealthRegionEnd;
|
||||
type Instruction = mango_v4::instruction::HealthRegionEnd;
|
||||
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 {};
|
||||
|
||||
let account = account_loader
|
||||
.load_mango_account(&self.account)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let health_check_metas = derive_health_check_remaining_account_metas(
|
||||
&account_loader,
|
||||
&account,
|
||||
self.affected_bank,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
account: self.account,
|
||||
};
|
||||
|
||||
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||
instruction.accounts.extend(health_check_metas.into_iter());
|
||||
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<&Keypair> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ use solana_sdk::signature::Keypair;
|
|||
|
||||
use super::mango_client::*;
|
||||
use super::solana::SolanaCookie;
|
||||
use super::{send_tx, MintCookie};
|
||||
use super::{send_tx, ClonableKeypair, MintCookie, UserCookie};
|
||||
|
||||
pub struct GroupWithTokensConfig<'a> {
|
||||
pub admin: &'a Keypair,
|
||||
|
@ -13,6 +13,7 @@ pub struct GroupWithTokensConfig<'a> {
|
|||
pub mints: &'a [MintCookie],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Token {
|
||||
pub index: u16,
|
||||
pub mint: MintCookie,
|
||||
|
@ -136,3 +137,48 @@ impl<'a> GroupWithTokensConfig<'a> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_funded_account(
|
||||
solana: &SolanaCookie,
|
||||
group: Pubkey,
|
||||
owner: &Keypair,
|
||||
account_num: u32,
|
||||
payer: &UserCookie,
|
||||
mints: &[MintCookie],
|
||||
amounts: u64,
|
||||
bank_index: usize,
|
||||
) -> Pubkey {
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer: &payer.key,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
for mint in mints {
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: amounts,
|
||||
account,
|
||||
token_account: payer.token_accounts[mint.index],
|
||||
token_authority: payer.key.clone(),
|
||||
bank_index,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
account
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ impl AddPacked for ProgramTest {
|
|||
|
||||
struct LoggerWrapper {
|
||||
inner: env_logger::Logger,
|
||||
program_log: Arc<RwLock<Vec<String>>>,
|
||||
capture: Arc<RwLock<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Log for LoggerWrapper {
|
||||
|
@ -67,7 +67,9 @@ impl Log for LoggerWrapper {
|
|||
{
|
||||
let msg = record.args().to_string();
|
||||
if let Some(data) = msg.strip_prefix("Program log: ") {
|
||||
self.program_log.write().unwrap().push(data.into());
|
||||
self.capture.write().unwrap().push(data.into());
|
||||
} else if let Some(data) = msg.strip_prefix("Program data: ") {
|
||||
self.capture.write().unwrap().push(data.into());
|
||||
}
|
||||
}
|
||||
self.inner.log(record);
|
||||
|
@ -85,14 +87,17 @@ pub struct MarginTradeCookie {
|
|||
|
||||
pub struct TestContextBuilder {
|
||||
test: ProgramTest,
|
||||
program_log_capture: Arc<RwLock<Vec<String>>>,
|
||||
logger_capture: Arc<RwLock<Vec<String>>>,
|
||||
mint0: Pubkey,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref LOGGER_CAPTURE: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(vec![]));
|
||||
static ref LOGGER_LOCK: Arc<RwLock<()>> = Arc::new(RwLock::new(()));
|
||||
}
|
||||
|
||||
impl TestContextBuilder {
|
||||
pub fn new() -> Self {
|
||||
let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry));
|
||||
|
||||
// We need to intercept logs to capture program log output
|
||||
let log_filter = "solana_rbpf=trace,\
|
||||
solana_runtime::message_processor=debug,\
|
||||
|
@ -102,18 +107,19 @@ impl TestContextBuilder {
|
|||
env_logger::Builder::from_env(env_logger::Env::new().default_filter_or(log_filter))
|
||||
.format_timestamp_nanos()
|
||||
.build();
|
||||
let program_log_capture = Arc::new(RwLock::new(vec![]));
|
||||
let _ = log::set_boxed_logger(Box::new(LoggerWrapper {
|
||||
inner: env_logger,
|
||||
program_log: program_log_capture.clone(),
|
||||
capture: LOGGER_CAPTURE.clone(),
|
||||
}));
|
||||
|
||||
let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry));
|
||||
|
||||
// intentionally set to as tight as possible, to catch potential problems early
|
||||
test.set_compute_max_units(87000);
|
||||
test.set_compute_max_units(75000);
|
||||
|
||||
Self {
|
||||
test,
|
||||
program_log_capture,
|
||||
logger_capture: LOGGER_CAPTURE.clone(),
|
||||
mint0: Pubkey::new_unique(),
|
||||
}
|
||||
}
|
||||
|
@ -277,7 +283,9 @@ impl TestContextBuilder {
|
|||
let solana = Arc::new(SolanaCookie {
|
||||
context: RefCell::new(context),
|
||||
rent,
|
||||
program_log: self.program_log_capture.clone(),
|
||||
logger_capture: self.logger_capture.clone(),
|
||||
logger_lock: LOGGER_LOCK.clone(),
|
||||
last_transaction_log: RefCell::new(vec![]),
|
||||
});
|
||||
|
||||
solana
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use std::{mem, sync::Arc};
|
||||
|
||||
use bytemuck::from_bytes;
|
||||
|
@ -41,7 +43,6 @@ pub struct SerumCookie {
|
|||
}
|
||||
|
||||
impl SerumCookie {
|
||||
#[allow(dead_code)]
|
||||
pub fn create_dex_account(&self, unpadded_len: usize) -> (Keypair, Instruction) {
|
||||
let serum_program_id = self.program_id;
|
||||
let key = Keypair::new();
|
||||
|
@ -57,7 +58,6 @@ impl SerumCookie {
|
|||
return (key, create_account_instr);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn gen_listing_params(
|
||||
&self,
|
||||
_coin_mint: &Pubkey,
|
||||
|
@ -94,7 +94,6 @@ impl SerumCookie {
|
|||
return (info, instructions);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn list_spot_market(
|
||||
&self,
|
||||
coin_mint: &MintCookie,
|
||||
|
@ -186,20 +185,19 @@ impl SerumCookie {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn consume_spot_events(
|
||||
&self,
|
||||
spot_market_cookie: &SpotMarketCookie,
|
||||
open_orders: Pubkey,
|
||||
open_orders: &[Pubkey],
|
||||
) {
|
||||
let instructions = [serum_dex::instruction::consume_events(
|
||||
&self.program_id,
|
||||
vec![&open_orders],
|
||||
open_orders.iter().collect(),
|
||||
&spot_market_cookie.market,
|
||||
&spot_market_cookie.event_q,
|
||||
&spot_market_cookie.coin_fee_account,
|
||||
&spot_market_cookie.pc_fee_account,
|
||||
5,
|
||||
10,
|
||||
)
|
||||
.unwrap()];
|
||||
self.solana
|
||||
|
@ -208,13 +206,11 @@ impl SerumCookie {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn strip_dex_padding(data: &[u8]) -> &[u8] {
|
||||
assert!(data.len() >= 12);
|
||||
&data[5..data.len() - 7]
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn load_open_orders(&self, open_orders: Pubkey) -> serum_dex::state::OpenOrders {
|
||||
let data = self.solana.get_account_data(open_orders).await.unwrap();
|
||||
let slice = Self::strip_dex_padding(&data);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
|
@ -17,17 +19,26 @@ use spl_token::*;
|
|||
pub struct SolanaCookie {
|
||||
pub context: RefCell<ProgramTestContext>,
|
||||
pub rent: Rent,
|
||||
pub program_log: Arc<RwLock<Vec<String>>>,
|
||||
pub logger_capture: Arc<RwLock<Vec<String>>>,
|
||||
pub logger_lock: Arc<RwLock<()>>,
|
||||
pub last_transaction_log: RefCell<Vec<String>>,
|
||||
}
|
||||
|
||||
impl SolanaCookie {
|
||||
#[allow(dead_code)]
|
||||
pub async fn process_transaction(
|
||||
&self,
|
||||
instructions: &[Instruction],
|
||||
signers: Option<&[&Keypair]>,
|
||||
) -> Result<(), BanksClientError> {
|
||||
self.program_log.write().unwrap().clear();
|
||||
// The locking in this function is convoluted:
|
||||
// We capture the program log output by overriding the global logger and capturing
|
||||
// messages there. This logger is potentially shared among multiple tests that run
|
||||
// concurrently.
|
||||
// To allow each independent SolanaCookie to capture only the logs from the transaction
|
||||
// passed to process_transaction, wo globally hold the "program_log_lock" for the
|
||||
// duration that the tx needs to process. So only a single one can run at a time.
|
||||
let tx_log_lock = Arc::new(self.logger_lock.write().unwrap());
|
||||
self.logger_capture.write().unwrap().clear();
|
||||
|
||||
let mut context = self.context.borrow_mut();
|
||||
|
||||
|
@ -45,13 +56,19 @@ impl SolanaCookie {
|
|||
|
||||
transaction.sign(&all_signers, context.last_blockhash);
|
||||
|
||||
context
|
||||
let result = context
|
||||
.banks_client
|
||||
.process_transaction_with_commitment(
|
||||
transaction,
|
||||
solana_sdk::commitment_config::CommitmentLevel::Processed,
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
|
||||
*self.last_transaction_log.borrow_mut() = self.logger_capture.read().unwrap().clone();
|
||||
|
||||
drop(tx_log_lock);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn get_clock(&self) -> solana_program::clock::Clock {
|
||||
|
@ -63,7 +80,6 @@ impl SolanaCookie {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn advance_by_slots(&self, slots: u64) {
|
||||
let clock = self.get_clock().await;
|
||||
self.context
|
||||
|
@ -72,8 +88,6 @@ impl SolanaCookie {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
pub async fn advance_clock(&self) {
|
||||
let mut clock = self.get_clock().await;
|
||||
let old_ts = clock.unix_timestamp;
|
||||
|
@ -133,7 +147,6 @@ impl SolanaCookie {
|
|||
key.pubkey()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn create_token_account(&self, owner: &Pubkey, mint: Pubkey) -> Pubkey {
|
||||
let keypair = Keypair::new();
|
||||
let rent = self.rent.minimum_balance(spl_token::state::Account::LEN);
|
||||
|
@ -162,7 +175,6 @@ impl SolanaCookie {
|
|||
}
|
||||
|
||||
// Note: Only one table can be created per authority per slot!
|
||||
#[allow(dead_code)]
|
||||
pub async fn create_address_lookup_table(
|
||||
&self,
|
||||
authority: &Keypair,
|
||||
|
@ -180,7 +192,6 @@ impl SolanaCookie {
|
|||
alt_address
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_account_data(&self, address: Pubkey) -> Option<Vec<u8>> {
|
||||
Some(
|
||||
self.context
|
||||
|
@ -194,7 +205,6 @@ impl SolanaCookie {
|
|||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_account_opt<T: AccountDeserialize>(&self, address: Pubkey) -> Option<T> {
|
||||
self.context
|
||||
.borrow_mut()
|
||||
|
@ -209,18 +219,30 @@ impl SolanaCookie {
|
|||
AccountDeserialize::try_deserialize(&mut data_slice).ok()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_account<T: AccountDeserialize>(&self, address: Pubkey) -> T {
|
||||
self.get_account_opt(address).await.unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn token_account_balance(&self, address: Pubkey) -> u64 {
|
||||
self.get_account::<TokenAccount>(address).await.amount
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn program_log(&self) -> Vec<String> {
|
||||
self.program_log.read().unwrap().clone()
|
||||
self.last_transaction_log.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn program_log_events<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
|
||||
&self,
|
||||
) -> Vec<T> {
|
||||
self.program_log()
|
||||
.iter()
|
||||
.filter_map(|data| {
|
||||
let bytes = base64::decode(data).ok()?;
|
||||
if bytes[0..8] != T::discriminator() {
|
||||
return None;
|
||||
}
|
||||
T::try_from_slice(&bytes[8..]).ok()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use bytemuck::{bytes_of, Contiguous};
|
||||
use solana_program::program_error::ProgramError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn gen_signer_seeds<'a>(nonce: &'a u64, acc_pk: &'a Pubkey) -> [&'a [u8]; 2] {
|
||||
[acc_pk.as_ref(), bytes_of(nonce)]
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn gen_signer_key(
|
||||
nonce: u64,
|
||||
acc_pk: &Pubkey,
|
||||
|
@ -19,7 +19,6 @@ pub fn gen_signer_key(
|
|||
Ok(Pubkey::create_program_address(&seeds, program_id)?)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn create_signer_key_and_nonce(program_id: &Pubkey, acc_pk: &Pubkey) -> (Pubkey, u64) {
|
||||
for i in 0..=u64::MAX_VALUE {
|
||||
if let Ok(pk) = gen_signer_key(i, acc_pk, program_id) {
|
||||
|
@ -29,7 +28,6 @@ pub fn create_signer_key_and_nonce(program_id: &Pubkey, acc_pk: &Pubkey) -> (Pub
|
|||
panic!("Could not generate signer key");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn clone_keypair(keypair: &Keypair) -> Keypair {
|
||||
Keypair::from_base58_string(&keypair.to_base58_string())
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ use solana_sdk::{
|
|||
use mango_v4::state::*;
|
||||
use program_test::*;
|
||||
|
||||
use mango_setup::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -27,7 +29,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -40,37 +42,18 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
|
|||
let collateral_token2 = &tokens[3];
|
||||
|
||||
// deposit some funds, to the vaults aren't empty
|
||||
let vault_account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 2,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
let vault_amount = 100000;
|
||||
for &token_account in payer_mint_accounts {
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: vault_amount,
|
||||
account: vault_account,
|
||||
token_account,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let vault_account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
2,
|
||||
&context.users[1],
|
||||
mints,
|
||||
vault_amount,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
// also add a tiny amount to bank0 for borrow_token1, so we can test multi-bank socialized loss
|
||||
send_tx(
|
||||
|
|
|
@ -111,15 +111,14 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
//
|
||||
// TEST: Compute the account health
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
ComputeAccountDataInstruction {
|
||||
account,
|
||||
health_type: HealthType::Init,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(solana, ComputeAccountDataInstruction { account })
|
||||
.await
|
||||
.unwrap();
|
||||
let health_data = solana
|
||||
.program_log_events::<mango_v4::events::MangoAccountData>()
|
||||
.pop()
|
||||
.unwrap();
|
||||
assert_eq!(health_data.init_health.to_num::<i64>(), 60);
|
||||
|
||||
//
|
||||
// TEST: Withdraw funds
|
||||
|
@ -143,6 +142,8 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
check_prev_instruction_post_health(&solana, account).await;
|
||||
|
||||
assert_eq!(solana.token_account_balance(vault).await, withdraw_amount);
|
||||
assert_eq!(
|
||||
solana.token_account_balance(payer_mint0_account).await,
|
||||
|
|
|
@ -6,6 +6,8 @@ use solana_sdk::{signature::Keypair, signature::Signer, transport::TransportErro
|
|||
use mango_v4::state::*;
|
||||
use program_test::*;
|
||||
|
||||
use mango_setup::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -24,7 +26,7 @@ async fn test_delegate() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group, register a token (mint0), create an account
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -33,36 +35,8 @@ async fn test_delegate() -> Result<(), TransportError> {
|
|||
.await;
|
||||
let bank = tokens[0].bank;
|
||||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
// deposit
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 100,
|
||||
account,
|
||||
token_account: payer_mint0_account,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let account =
|
||||
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 100, 0).await;
|
||||
|
||||
//
|
||||
// TEST: Edit account - Set delegate
|
||||
|
|
|
@ -7,6 +7,8 @@ use solana_sdk::{signature::Keypair, transport::TransportError};
|
|||
|
||||
use program_test::*;
|
||||
|
||||
use mango_setup::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
// Try to reach compute limits in health checks by having many different tokens in an account
|
||||
|
@ -19,13 +21,12 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
|
|||
let owner = &context.users[0].key;
|
||||
let payer = &context.users[1].key;
|
||||
let mints = &context.mints[0..10];
|
||||
let payer_mint_accounts = &context.users[1].token_accounts[0..mints.len()];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, .. } = mango_setup::GroupWithTokensConfig {
|
||||
let GroupWithTokens { group, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -33,43 +34,8 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
|
|||
.create(solana)
|
||||
.await;
|
||||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
//
|
||||
// TEST: Deposit user funds for all the mints
|
||||
// each deposit will end with a health check
|
||||
//
|
||||
for &token_account in payer_mint_accounts {
|
||||
let deposit_amount = 1000;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit_amount,
|
||||
account,
|
||||
token_account,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
// each deposit ends with a health check
|
||||
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 1000, 0).await;
|
||||
|
||||
// TODO: actual explicit CU comparisons.
|
||||
// On 2022-5-25 the final deposit costs 36905 CU and each new token increases it by roughly 1600 CU
|
||||
|
@ -216,36 +182,17 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
|||
.create(solana)
|
||||
.await;
|
||||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
&mints[..1],
|
||||
1000,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
// Give the account some quote currency
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 1000,
|
||||
account,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: Create perp markets
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{signature::Keypair, transport::TransportError};
|
||||
|
||||
use mango_v4::state::MangoAccount;
|
||||
|
||||
use program_test::*;
|
||||
|
||||
use mango_setup::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_wrap() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = &Keypair::new();
|
||||
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;
|
||||
|
||||
//
|
||||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
// SETUP: Create an account with deposits, so the second account can borrow more than it has
|
||||
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 1000, 0).await;
|
||||
|
||||
// SETUP: Make a second account
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 1,
|
||||
token_count: 8,
|
||||
serum3_count: 0,
|
||||
perp_count: 0,
|
||||
perp_oo_count: 0,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
// SETUP: deposit something, so only one new token position needs to be created
|
||||
// simply because the test code can't deal with two affected banks right now
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 1,
|
||||
account,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let send_test_tx = |repay_amount| {
|
||||
let tokens = tokens.clone();
|
||||
async move {
|
||||
let mut tx = ClientTransaction::new(solana);
|
||||
tx.add_instruction(HealthRegionBeginInstruction { account })
|
||||
.await;
|
||||
tx.add_instruction(TokenWithdrawInstruction {
|
||||
amount: 1000, // more than the 1 token that's on the account
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 0,
|
||||
})
|
||||
.await;
|
||||
tx.add_instruction(TokenDepositInstruction {
|
||||
amount: repay_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
})
|
||||
.await;
|
||||
tx.add_instruction(HealthRegionEndInstruction {
|
||||
account,
|
||||
affected_bank: Some(tokens[1].bank),
|
||||
})
|
||||
.await;
|
||||
tx.send().await
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// TEST: Borrow a lot of token0 without collateral, but repay too little
|
||||
//
|
||||
{
|
||||
send_test_tx(1000).await.unwrap_err();
|
||||
let logs = solana.program_log();
|
||||
// reaches the End instruction
|
||||
assert!(logs
|
||||
.iter()
|
||||
.any(|line| line.contains("Instruction: HealthRegionEnd")));
|
||||
// errors due to health
|
||||
assert!(logs
|
||||
.iter()
|
||||
.any(|line| line.contains("Error Code: HealthMustBePositiveOrIncrease")));
|
||||
}
|
||||
|
||||
//
|
||||
// TEST: Borrow a lot of token0 without collateral, and repay in token1 in the same tx
|
||||
//
|
||||
{
|
||||
let start_payer_mint0 = solana.token_account_balance(payer_mint_accounts[0]).await;
|
||||
let start_payer_mint1 = solana.token_account_balance(payer_mint_accounts[1]).await;
|
||||
|
||||
send_test_tx(3000).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
solana.token_account_balance(payer_mint_accounts[0]).await - start_payer_mint0,
|
||||
1000
|
||||
);
|
||||
assert_eq!(
|
||||
start_payer_mint1 - solana.token_account_balance(payer_mint_accounts[1]).await,
|
||||
3000
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, account, tokens[0].bank).await,
|
||||
-999
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, account, tokens[1].bank).await,
|
||||
3000
|
||||
);
|
||||
let account_data: MangoAccount = solana.get_account(account).await;
|
||||
assert_eq!(account_data.in_health_region, 0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -7,11 +7,15 @@ use solana_sdk::{signature::Keypair, transport::TransportError};
|
|||
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
|
||||
use program_test::*;
|
||||
|
||||
use mango_setup::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = &Keypair::new();
|
||||
|
@ -24,7 +28,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -35,36 +39,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
let quote_token = &tokens[1];
|
||||
|
||||
// deposit some funds, to the vaults aren't empty
|
||||
let vault_account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 2,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
for &token_account in payer_mint_accounts {
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 10000,
|
||||
account: vault_account,
|
||||
token_account,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 10000, 0).await;
|
||||
|
||||
//
|
||||
// SETUP: Create serum market
|
||||
|
@ -94,36 +69,18 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
//
|
||||
// SETUP: Make an account and deposit some quote
|
||||
//
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
let deposit_amount = 1000;
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
&mints[1..2],
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: Create an open orders account and an order
|
||||
|
|
|
@ -6,6 +6,8 @@ use solana_sdk::signature::Signer;
|
|||
|
||||
use program_test::*;
|
||||
|
||||
use mango_setup::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
// This is an unspecific happy-case test that just runs a few instructions to check
|
||||
|
@ -21,7 +23,6 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
|
|||
let payer = &context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
let payer_mint0_account = context.users[1].token_accounts[0];
|
||||
let payer_mint1_account = context.users[1].token_accounts[1];
|
||||
let loan_origination_fee = 0.0005;
|
||||
|
||||
// higher resolution that the loan_origination_fee for one token
|
||||
|
@ -31,7 +32,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
|
|||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -45,48 +46,17 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
|
|||
// provide some funds for tokens, so the test user can borrow
|
||||
//
|
||||
let provided_amount = 1000;
|
||||
|
||||
let provider_account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 1,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
mints,
|
||||
provided_amount,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: provided_amount,
|
||||
account: provider_account,
|
||||
token_account: payer_mint0_account,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: provided_amount,
|
||||
account: provider_account,
|
||||
token_account: payer_mint1_account,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
|
||||
//
|
||||
// create thes test user account
|
||||
|
|
|
@ -7,6 +7,8 @@ use program_test::*;
|
|||
use solana_program_test::*;
|
||||
use solana_sdk::{signature::Keypair, signer::Signer, transport::TransportError};
|
||||
|
||||
use mango_setup::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -18,13 +20,12 @@ async fn test_perp() -> 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];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -32,102 +33,29 @@ async fn test_perp() -> 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 deposit_amount = 1000;
|
||||
let account_0 = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
mints,
|
||||
deposit_amount,
|
||||
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,
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
//
|
||||
// SETUP: Deposit user funds
|
||||
//
|
||||
{
|
||||
let deposit_amount = 1000;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit_amount,
|
||||
account: account_0,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit_amount,
|
||||
account: account_0,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
let deposit_amount = 1000;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit_amount,
|
||||
account: account_1,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit_amount,
|
||||
account: account_1,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
.await;
|
||||
|
||||
//
|
||||
// TEST: Create a perp market
|
||||
|
@ -204,6 +132,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_0).await;
|
||||
|
||||
let order_id_to_cancel = solana
|
||||
.get_account::<MangoAccount>(account_0)
|
||||
|
@ -250,6 +179,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_0).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
|
@ -291,6 +221,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_0).await;
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
|
@ -311,6 +242,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_0).await;
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
|
@ -331,6 +263,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_0).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
|
@ -371,6 +304,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_0).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
|
@ -392,6 +326,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_1).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
|
|
|
@ -6,6 +6,8 @@ use solana_sdk::signature::Keypair;
|
|||
|
||||
use program_test::*;
|
||||
|
||||
use crate::mango_setup::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
// Check opening and closing positions
|
||||
|
@ -50,43 +52,18 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
.unwrap()
|
||||
.account;
|
||||
|
||||
let funding_account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 1,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
let funding_amount = 1000000;
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
mints,
|
||||
funding_amount,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
//
|
||||
// SETUP: Put some tokens into the funding account to allow borrowing
|
||||
//
|
||||
{
|
||||
let funding_amount = 1000000;
|
||||
for &payer_token in payer_mint_accounts {
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: funding_amount,
|
||||
account: funding_account,
|
||||
token_account: payer_token,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
.await;
|
||||
|
||||
//
|
||||
// TEST: Deposit and withdraw tokens for all mints
|
||||
|
|
|
@ -1,29 +1,169 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{signature::Keypair, signer::Signer, transport::TransportError};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transport::TransportError};
|
||||
|
||||
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
|
||||
use mango_v4::instructions::{
|
||||
OpenOrdersSlim, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side,
|
||||
};
|
||||
use mango_v4::state::Serum3Orders;
|
||||
use program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
use mango_setup::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct SerumOrderPlacer {
|
||||
solana: Arc<SolanaCookie>,
|
||||
serum: Arc<SerumCookie>,
|
||||
account: Pubkey,
|
||||
owner: Keypair,
|
||||
serum_market: Pubkey,
|
||||
open_orders: Pubkey,
|
||||
next_client_order_id: u64,
|
||||
}
|
||||
|
||||
impl SerumOrderPlacer {
|
||||
fn inc_client_order_id(&mut self) -> u64 {
|
||||
let id = self.next_client_order_id;
|
||||
self.next_client_order_id += 1;
|
||||
id
|
||||
}
|
||||
|
||||
async fn find_order_id_for_client_order_id(&self, client_order_id: u64) -> Option<(u128, u64)> {
|
||||
let open_orders = self.serum.load_open_orders(self.open_orders).await;
|
||||
for i in 0..128 {
|
||||
if open_orders.free_slot_bits & (1u128 << i) != 0 {
|
||||
continue;
|
||||
}
|
||||
if open_orders.client_order_ids[i] == client_order_id {
|
||||
return Some((open_orders.orders[i], client_order_id));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn bid(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
|
||||
let client_order_id = self.inc_client_order_id();
|
||||
send_tx(
|
||||
&self.solana,
|
||||
Serum3PlaceOrderInstruction {
|
||||
side: Serum3Side::Bid,
|
||||
limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100)
|
||||
max_base_qty: max_base / 100, // in base lot (100)
|
||||
max_native_quote_qty_including_fees: (limit_price * (max_base as f64)) as u64,
|
||||
self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction,
|
||||
order_type: Serum3OrderType::Limit,
|
||||
client_order_id,
|
||||
limit: 10,
|
||||
account: self.account,
|
||||
owner: &self.owner,
|
||||
serum_market: self.serum_market,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
self.find_order_id_for_client_order_id(client_order_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn ask(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
|
||||
let client_order_id = self.inc_client_order_id();
|
||||
send_tx(
|
||||
&self.solana,
|
||||
Serum3PlaceOrderInstruction {
|
||||
side: Serum3Side::Ask,
|
||||
limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100)
|
||||
max_base_qty: max_base / 100, // in base lot (100)
|
||||
max_native_quote_qty_including_fees: (limit_price * (max_base as f64)) as u64,
|
||||
self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction,
|
||||
order_type: Serum3OrderType::Limit,
|
||||
client_order_id,
|
||||
limit: 10,
|
||||
account: self.account,
|
||||
owner: &self.owner,
|
||||
serum_market: self.serum_market,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
self.find_order_id_for_client_order_id(client_order_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn cancel(&self, order_id: u128) {
|
||||
let side = {
|
||||
let open_orders = self.serum.load_open_orders(self.open_orders).await;
|
||||
let orders = open_orders.orders;
|
||||
let idx = orders.iter().position(|&v| v == order_id).unwrap();
|
||||
if open_orders.is_bid_bits & (1u128 << idx) == 0 {
|
||||
Serum3Side::Ask
|
||||
} else {
|
||||
Serum3Side::Bid
|
||||
}
|
||||
};
|
||||
send_tx(
|
||||
&self.solana,
|
||||
Serum3CancelOrderInstruction {
|
||||
side,
|
||||
order_id,
|
||||
account: self.account,
|
||||
owner: &self.owner,
|
||||
serum_market: self.serum_market,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn settle(&self) {
|
||||
// to avoid multiple settles looking like the same tx
|
||||
self.solana.advance_by_slots(1).await;
|
||||
send_tx(
|
||||
&self.solana,
|
||||
Serum3SettleFundsInstruction {
|
||||
account: self.account,
|
||||
owner: &self.owner,
|
||||
serum_market: self.serum_market,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn mango_serum_orders(&self) -> Serum3Orders {
|
||||
let account_data = get_mango_account(&self.solana, self.account).await;
|
||||
let orders = account_data
|
||||
.all_serum3_orders()
|
||||
.find(|s| s.open_orders == self.open_orders)
|
||||
.unwrap();
|
||||
orders.clone()
|
||||
}
|
||||
|
||||
async fn open_orders(&self) -> OpenOrdersSlim {
|
||||
OpenOrdersSlim::from_oo(&self.serum.load_open_orders(self.open_orders).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serum() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
async fn test_serum_basics() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = &Keypair::new();
|
||||
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];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -33,23 +173,6 @@ async fn test_serum() -> Result<(), TransportError> {
|
|||
let base_token = &tokens[0];
|
||||
let quote_token = &tokens[1];
|
||||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
//
|
||||
// SETUP: Create serum market
|
||||
//
|
||||
|
@ -58,39 +181,6 @@ async fn test_serum() -> Result<(), TransportError> {
|
|||
.list_spot_market(&base_token.mint, "e_token.mint)
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: Deposit user funds
|
||||
//
|
||||
{
|
||||
let deposit_amount = 1000;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
//
|
||||
// TEST: Register a serum market
|
||||
//
|
||||
|
@ -111,6 +201,22 @@ async fn test_serum() -> Result<(), TransportError> {
|
|||
.unwrap()
|
||||
.serum_market;
|
||||
|
||||
//
|
||||
// SETUP: Create account
|
||||
//
|
||||
let deposit_amount = 1000;
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
mints,
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// TEST: Create an open orders account
|
||||
//
|
||||
|
@ -136,89 +242,74 @@ async fn test_serum() -> Result<(), TransportError> {
|
|||
[(open_orders, 0)]
|
||||
);
|
||||
|
||||
let mut order_placer = SerumOrderPlacer {
|
||||
solana: solana.clone(),
|
||||
serum: context.serum.clone(),
|
||||
account,
|
||||
owner: owner.clone(),
|
||||
serum_market,
|
||||
open_orders,
|
||||
next_client_order_id: 0,
|
||||
};
|
||||
|
||||
//
|
||||
// TEST: Place an order
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
Serum3PlaceOrderInstruction {
|
||||
side: Serum3Side::Bid,
|
||||
limit_price: 10, // in quote_lot (10) per base lot (100)
|
||||
max_base_qty: 1, // in base lot (100)
|
||||
max_native_quote_qty_including_fees: 100,
|
||||
self_trade_behavior: Serum3SelfTradeBehavior::DecrementTake,
|
||||
order_type: Serum3OrderType::Limit,
|
||||
client_order_id: 0,
|
||||
limit: 10,
|
||||
account,
|
||||
owner,
|
||||
serum_market,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let (order_id, _) = order_placer.bid(1.0, 100).await.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account).await;
|
||||
|
||||
let native0 = account_position(solana, account, base_token.bank).await;
|
||||
let native1 = account_position(solana, account, quote_token.bank).await;
|
||||
assert_eq!(native0, 1000);
|
||||
assert_eq!(native1, 900);
|
||||
|
||||
// get the order id
|
||||
let open_orders_bytes = solana.get_account_data(open_orders).await.unwrap();
|
||||
let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes(
|
||||
&open_orders_bytes[5..5 + std::mem::size_of::<serum_dex::state::OpenOrders>()],
|
||||
);
|
||||
let order_id = open_orders_data.orders[0];
|
||||
let account_data = get_mango_account(solana, account).await;
|
||||
assert_eq!(account_data.token_position_by_raw_index(0).in_use_count, 1);
|
||||
assert_eq!(account_data.token_position_by_raw_index(1).in_use_count, 1);
|
||||
assert_eq!(account_data.token_position_by_raw_index(2).in_use_count, 0);
|
||||
let serum_orders = account_data.serum3_orders_by_raw_index(0);
|
||||
assert_eq!(serum_orders.base_borrows_without_fee, 0);
|
||||
assert_eq!(serum_orders.quote_borrows_without_fee, 0);
|
||||
|
||||
assert!(order_id != 0);
|
||||
|
||||
//
|
||||
// TEST: Cancel the order
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
Serum3CancelOrderInstruction {
|
||||
side: Serum3Side::Bid,
|
||||
order_id,
|
||||
account,
|
||||
owner,
|
||||
serum_market,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
order_placer.cancel(order_id).await;
|
||||
|
||||
//
|
||||
// TEST: Settle, moving the freed up funds back
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
Serum3SettleFundsInstruction {
|
||||
account,
|
||||
owner,
|
||||
serum_market,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
order_placer.settle().await;
|
||||
|
||||
let native0 = account_position(solana, account, base_token.bank).await;
|
||||
let native1 = account_position(solana, account, quote_token.bank).await;
|
||||
assert_eq!(native0, 1000);
|
||||
assert_eq!(native1, 1000);
|
||||
|
||||
// Process events such that the OutEvent deactivates the closed order on open_orders
|
||||
context
|
||||
.serum
|
||||
.consume_spot_events(&serum_market_cookie, &[open_orders])
|
||||
.await;
|
||||
|
||||
// close oo account
|
||||
// TODO: custom program error: 0x2a TooManyOpenOrders https://github.com/project-serum/serum-dex/blob/master/dex/src/error.rs#L88
|
||||
// send_tx(
|
||||
// solana,
|
||||
// Serum3CloseOpenOrdersInstruction {
|
||||
// account,
|
||||
// serum_market,
|
||||
// owner,
|
||||
// sol_destination: payer.pubkey(),
|
||||
// },
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
Serum3CloseOpenOrdersInstruction {
|
||||
account,
|
||||
serum_market,
|
||||
owner,
|
||||
sol_destination: payer.pubkey(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let account_data = get_mango_account(solana, account).await;
|
||||
assert_eq!(account_data.token_position_by_raw_index(0).in_use_count, 0);
|
||||
assert_eq!(account_data.token_position_by_raw_index(1).in_use_count, 0);
|
||||
|
||||
// deregister serum3 market
|
||||
send_tx(
|
||||
|
@ -235,3 +326,257 @@ async fn test_serum() -> Result<(), TransportError> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = &Keypair::new();
|
||||
let owner = &context.users[0].key;
|
||||
let payer = &context.users[1].key;
|
||||
let mints = &context.mints[0..3];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let base_token = &tokens[0];
|
||||
let base_bank = base_token.bank;
|
||||
let quote_token = &tokens[1];
|
||||
let quote_bank = quote_token.bank;
|
||||
|
||||
//
|
||||
// SETUP: Create serum market
|
||||
//
|
||||
let serum_market_cookie = context
|
||||
.serum
|
||||
.list_spot_market(&base_token.mint, "e_token.mint)
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: Register a serum market
|
||||
//
|
||||
let serum_market = send_tx(
|
||||
solana,
|
||||
Serum3RegisterMarketInstruction {
|
||||
group,
|
||||
admin,
|
||||
serum_program: context.serum.program_id,
|
||||
serum_market_external: serum_market_cookie.market,
|
||||
market_index: 0,
|
||||
base_bank: base_token.bank,
|
||||
quote_bank: quote_token.bank,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.serum_market;
|
||||
|
||||
//
|
||||
// SETUP: Create accounts
|
||||
//
|
||||
let deposit_amount = 180000;
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
mints,
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
let account2 = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
2,
|
||||
&context.users[1],
|
||||
mints,
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
// to have enough funds in the vaults
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
3,
|
||||
&context.users[1],
|
||||
mints,
|
||||
10000000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
let open_orders = send_tx(
|
||||
solana,
|
||||
Serum3CreateOpenOrdersInstruction {
|
||||
account,
|
||||
serum_market,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.open_orders;
|
||||
|
||||
let open_orders2 = send_tx(
|
||||
solana,
|
||||
Serum3CreateOpenOrdersInstruction {
|
||||
account: account2,
|
||||
serum_market,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.open_orders;
|
||||
|
||||
let mut order_placer = SerumOrderPlacer {
|
||||
solana: solana.clone(),
|
||||
serum: context.serum.clone(),
|
||||
account,
|
||||
owner: owner.clone(),
|
||||
serum_market,
|
||||
open_orders,
|
||||
next_client_order_id: 0,
|
||||
};
|
||||
let mut order_placer2 = SerumOrderPlacer {
|
||||
solana: solana.clone(),
|
||||
serum: context.serum.clone(),
|
||||
account: account2,
|
||||
owner: owner.clone(),
|
||||
serum_market,
|
||||
open_orders: open_orders2,
|
||||
next_client_order_id: 100000,
|
||||
};
|
||||
|
||||
//
|
||||
// TEST: Placing and canceling an order does not take loan origination fees even if borrows are needed
|
||||
//
|
||||
{
|
||||
let (bid_order_id, _) = order_placer.bid(1.0, 200000).await.unwrap();
|
||||
let (ask_order_id, _) = order_placer.ask(2.0, 200000).await.unwrap();
|
||||
|
||||
let o = order_placer.mango_serum_orders().await;
|
||||
assert_eq!(o.base_borrows_without_fee, 19999); // rounded
|
||||
assert_eq!(o.quote_borrows_without_fee, 19999);
|
||||
|
||||
order_placer.cancel(bid_order_id).await;
|
||||
order_placer.cancel(ask_order_id).await;
|
||||
|
||||
let o = order_placer.mango_serum_orders().await;
|
||||
assert_eq!(o.base_borrows_without_fee, 19999); // unchanged
|
||||
assert_eq!(o.quote_borrows_without_fee, 19999);
|
||||
|
||||
// placing new, slightly larger orders increases the borrow_without_fee amount only by a small amount
|
||||
let (bid_order_id, _) = order_placer.bid(1.0, 210000).await.unwrap();
|
||||
let (ask_order_id, _) = order_placer.ask(2.0, 300000).await.unwrap();
|
||||
|
||||
let o = order_placer.mango_serum_orders().await;
|
||||
assert_eq!(o.base_borrows_without_fee, 119998); // rounded
|
||||
assert_eq!(o.quote_borrows_without_fee, 29998);
|
||||
|
||||
order_placer.cancel(bid_order_id).await;
|
||||
order_placer.cancel(ask_order_id).await;
|
||||
|
||||
// returns all the funds
|
||||
order_placer.settle().await;
|
||||
|
||||
let o = order_placer.mango_serum_orders().await;
|
||||
assert_eq!(o.base_borrows_without_fee, 0);
|
||||
assert_eq!(o.quote_borrows_without_fee, 0);
|
||||
|
||||
assert_eq!(
|
||||
account_position(solana, account, quote_bank).await,
|
||||
deposit_amount as i64
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, account, base_bank).await,
|
||||
deposit_amount as i64
|
||||
);
|
||||
|
||||
// consume all the out events from the cancels
|
||||
context
|
||||
.serum
|
||||
.consume_spot_events(&serum_market_cookie, &[open_orders])
|
||||
.await;
|
||||
}
|
||||
|
||||
let without_serum_taker_fee = |amount: i64| (amount as f64 * (1.0 - 0.0022)).trunc() as i64;
|
||||
let serum_maker_rebate = |amount: i64| (amount as f64 * 0.0003).round() as i64;
|
||||
let loan_origination_fee = |amount: i64| (amount as f64 * 0.0005).trunc() as i64;
|
||||
|
||||
//
|
||||
// TEST: Order execution and settling charges borrow fee
|
||||
//
|
||||
{
|
||||
let deposit_amount = deposit_amount as i64;
|
||||
let bid_amount = 200000;
|
||||
let ask_amount = 210000;
|
||||
let fill_amount = 200000;
|
||||
|
||||
// account2 has an order on the book
|
||||
order_placer2.bid(1.0, bid_amount as u64).await.unwrap();
|
||||
|
||||
// account takes
|
||||
order_placer.ask(1.0, ask_amount as u64).await.unwrap();
|
||||
order_placer.settle().await;
|
||||
|
||||
let o = order_placer.mango_serum_orders().await;
|
||||
// parts of the order ended up on the book an may cause loan origination fees later
|
||||
assert_eq!(
|
||||
o.base_borrows_without_fee,
|
||||
(ask_amount - fill_amount) as u64
|
||||
);
|
||||
assert_eq!(o.quote_borrows_without_fee, 0);
|
||||
|
||||
assert_eq!(
|
||||
account_position(solana, account, quote_bank).await,
|
||||
deposit_amount + without_serum_taker_fee(fill_amount)
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, account, base_bank).await,
|
||||
deposit_amount - ask_amount - loan_origination_fee(fill_amount - deposit_amount)
|
||||
);
|
||||
|
||||
// check account2 balances too
|
||||
context
|
||||
.serum
|
||||
.consume_spot_events(&serum_market_cookie, &[open_orders, open_orders2])
|
||||
.await;
|
||||
order_placer2.settle().await;
|
||||
|
||||
let o = order_placer2.mango_serum_orders().await;
|
||||
assert_eq!(o.base_borrows_without_fee, 0);
|
||||
assert_eq!(o.quote_borrows_without_fee, 0);
|
||||
|
||||
assert_eq!(
|
||||
account_position(solana, account2, base_bank).await,
|
||||
deposit_amount + fill_amount
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, account2, quote_bank).await,
|
||||
deposit_amount - fill_amount - loan_origination_fee(fill_amount - deposit_amount)
|
||||
+ serum_maker_rebate(fill_amount)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use mango_v4::state::*;
|
|||
use solana_program_test::*;
|
||||
use solana_sdk::{signature::Keypair, transport::TransportError};
|
||||
|
||||
use mango_setup::*;
|
||||
use program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
@ -17,13 +18,12 @@ async fn test_token_update_index_and_rate() -> 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];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -32,66 +32,18 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> {
|
|||
.await;
|
||||
|
||||
// deposit some funds, to the vaults aren't empty
|
||||
let deposit_account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 10000, 0).await;
|
||||
let withdraw_account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
&mints[1..2],
|
||||
100000,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
for &token_account in payer_mint_accounts {
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 10000,
|
||||
account: deposit_account,
|
||||
token_account,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let withdraw_account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 1,
|
||||
token_count: 16,
|
||||
serum3_count: 8,
|
||||
perp_count: 8,
|
||||
perp_oo_count: 8,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 100000,
|
||||
account: withdraw_account,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
|
|
|
@ -197,16 +197,16 @@ export class I80F48 {
|
|||
return this.data.cmp(x.getData());
|
||||
}
|
||||
neg(): I80F48 {
|
||||
return this.mul(NEG_ONE_I80F48);
|
||||
return this.mul(_NEG_ONE_I80F48);
|
||||
}
|
||||
isPos(): boolean {
|
||||
return this.gt(ZERO_I80F48);
|
||||
return this.gt(_ZERO_I80F48);
|
||||
}
|
||||
isNeg(): boolean {
|
||||
return this.data.isNeg();
|
||||
}
|
||||
isZero(): boolean {
|
||||
return this.eq(ZERO_I80F48);
|
||||
return this.eq(_ZERO_I80F48);
|
||||
}
|
||||
min(x: I80F48): I80F48 {
|
||||
return this.lte(x) ? this : x;
|
||||
|
@ -224,10 +224,22 @@ export class I80F48 {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
export const ONE_I80F48 = I80F48.fromString('1');
|
||||
const _ZERO_I80F48 = I80F48.fromNumber(0);
|
||||
/** @internal */
|
||||
export const ZERO_I80F48 = I80F48.fromString('0');
|
||||
/** @internal */
|
||||
export const NEG_ONE_I80F48 = I80F48.fromString('-1');
|
||||
export const HUNDRED_I80F48 = I80F48.fromString('100');
|
||||
export const MAX_I80F48 = new I80F48(I80F48.MAX_BN);
|
||||
const _NEG_ONE_I80F48 = I80F48.fromNumber(-1);
|
||||
|
||||
export function ONE_I80F48(): I80F48 {
|
||||
return I80F48.fromNumber(1);
|
||||
}
|
||||
|
||||
export function ZERO_I80F48(): I80F48 {
|
||||
return I80F48.fromNumber(0);
|
||||
}
|
||||
|
||||
export function HUNDRED_I80F48(): I80F48 {
|
||||
return I80F48.fromNumber(100);
|
||||
}
|
||||
|
||||
export function MAX_I80F48(): I80F48 {
|
||||
return new I80F48(I80F48.MAX_BN);
|
||||
}
|
||||
|
|
|
@ -286,8 +286,8 @@ export class Bank {
|
|||
const totalBorrows = this.nativeBorrows();
|
||||
const totalDeposits = this.nativeDeposits();
|
||||
|
||||
if (totalDeposits.eq(ZERO_I80F48) && totalBorrows.eq(ZERO_I80F48)) {
|
||||
return ZERO_I80F48;
|
||||
if (totalDeposits.isZero() && totalBorrows.isZero()) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
if (totalDeposits.lte(totalBorrows)) {
|
||||
return this.maxRate;
|
||||
|
@ -329,9 +329,9 @@ export class Bank {
|
|||
const totalBorrows = this.nativeBorrows();
|
||||
const totalDeposits = this.nativeDeposits();
|
||||
|
||||
if (totalDeposits.eq(ZERO_I80F48) && totalBorrows.eq(ZERO_I80F48)) {
|
||||
return ZERO_I80F48;
|
||||
} else if (totalDeposits.eq(ZERO_I80F48)) {
|
||||
if (totalDeposits.isZero() && totalBorrows.isZero()) {
|
||||
return ZERO_I80F48();
|
||||
} else if (totalDeposits.isZero()) {
|
||||
return this.maxRate;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { BorshAccountsCoder } from '@project-serum/anchor';
|
||||
import { coder } from '@project-serum/anchor/dist/cjs/spl/token';
|
||||
import { Market } from '@project-serum/serum';
|
||||
import {
|
||||
getFeeRates,
|
||||
getFeeTier,
|
||||
Market,
|
||||
Orderbook,
|
||||
} from '@project-serum/serum';
|
||||
import { parsePriceData, PriceData } from '@pythnetwork/client';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
|
@ -68,8 +73,9 @@ export class Group {
|
|||
public banksMapByName: Map<string, Bank[]>,
|
||||
public banksMapByMint: Map<string, Bank[]>,
|
||||
public banksMapByTokenIndex: Map<number, Bank[]>,
|
||||
public serum3MarketsMap: Map<string, Serum3Market>,
|
||||
public serum3MarketsMapByExternal: Map<string, Serum3Market>,
|
||||
public serum3MarketExternalsMap: Map<string, Market>,
|
||||
// TODO rethink key
|
||||
public perpMarketsMap: Map<string, PerpMarket>,
|
||||
public mintInfosMapByTokenIndex: Map<number, MintInfo>,
|
||||
public mintInfosMapByMint: Map<string, MintInfo>,
|
||||
|
@ -77,12 +83,6 @@ export class Group {
|
|||
public vaultAmountsMap: Map<string, number>,
|
||||
) {}
|
||||
|
||||
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
|
||||
return Array.from(this.serum3MarketsMap.values()).find(
|
||||
(serum3Market) => serum3Market.marketIndex === marketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
public async reloadAll(client: MangoClient) {
|
||||
let ids: Id | undefined = undefined;
|
||||
|
||||
|
@ -180,14 +180,17 @@ export class Group {
|
|||
serum3Markets = await client.serum3GetMarkets(this);
|
||||
}
|
||||
|
||||
this.serum3MarketsMap = new Map(
|
||||
serum3Markets.map((serum3Market) => [serum3Market.name, serum3Market]),
|
||||
this.serum3MarketsMapByExternal = new Map(
|
||||
serum3Markets.map((serum3Market) => [
|
||||
serum3Market.serumMarketExternal.toBase58(),
|
||||
serum3Market,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
public async reloadSerum3ExternalMarkets(client: MangoClient, ids?: Id) {
|
||||
const externalMarkets = await Promise.all(
|
||||
Array.from(this.serum3MarketsMap.values()).map((serum3Market) =>
|
||||
Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) =>
|
||||
Market.load(
|
||||
client.program.provider.connection,
|
||||
serum3Market.serumMarketExternal,
|
||||
|
@ -198,10 +201,12 @@ export class Group {
|
|||
);
|
||||
|
||||
this.serum3MarketExternalsMap = new Map(
|
||||
Array.from(this.serum3MarketsMap.values()).map((serum3Market, index) => [
|
||||
serum3Market.name,
|
||||
externalMarkets[index],
|
||||
]),
|
||||
Array.from(this.serum3MarketsMapByExternal.values()).map(
|
||||
(serum3Market, index) => [
|
||||
serum3Market.serumMarketExternal.toBase58(),
|
||||
externalMarkets[index],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -237,7 +242,7 @@ export class Group {
|
|||
for (const [index, price] of prices.entries()) {
|
||||
for (const bank of banks[index]) {
|
||||
if (bank.name === 'USDC') {
|
||||
bank.price = ONE_I80F48;
|
||||
bank.price = ONE_I80F48();
|
||||
bank.uiPrice = 1;
|
||||
} else {
|
||||
// TODO: Implement switchboard oracle type
|
||||
|
@ -299,14 +304,14 @@ export class Group {
|
|||
);
|
||||
}
|
||||
|
||||
public getMintDecimals(mintPk: PublicKey) {
|
||||
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()}`);
|
||||
return banks[0].mintDecimals;
|
||||
}
|
||||
|
||||
public getFirstBankByMint(mintPk: PublicKey) {
|
||||
public getFirstBankByMint(mintPk: PublicKey): Bank {
|
||||
const banks = this.banksMapByMint.get(mintPk.toString());
|
||||
if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`);
|
||||
return banks[0];
|
||||
|
@ -340,6 +345,49 @@ export class Group {
|
|||
return I80F48.fromNumber(totalAmount);
|
||||
}
|
||||
|
||||
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
|
||||
return Array.from(this.serum3MarketsMapByExternal.values()).find(
|
||||
(serum3Market) => serum3Market.marketIndex === marketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadSerum3BidsForMarket(
|
||||
client: MangoClient,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<Orderbook> {
|
||||
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<Orderbook> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param mintPk
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
ZERO_I80F48,
|
||||
} from './I80F48';
|
||||
import { HealthType } from './mangoAccount';
|
||||
import { Serum3Market, Serum3Side } from './serum3';
|
||||
|
||||
// ░░░░
|
||||
//
|
||||
|
@ -37,42 +38,46 @@ import { HealthType } from './mangoAccount';
|
|||
// warning: this code is copy pasta from rust, keep in sync with health.rs
|
||||
|
||||
export class HealthCache {
|
||||
tokenInfos: TokenInfo[];
|
||||
serum3Infos: Serum3Info[];
|
||||
perpInfos: PerpInfo[];
|
||||
constructor(
|
||||
public tokenInfos: TokenInfo[],
|
||||
public serum3Infos: Serum3Info[],
|
||||
public perpInfos: PerpInfo[],
|
||||
) {}
|
||||
|
||||
constructor(dto: HealthCacheDto) {
|
||||
this.tokenInfos = dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto));
|
||||
this.serum3Infos = dto.serum3Infos.map((dto) => new Serum3Info(dto));
|
||||
this.perpInfos = dto.perpInfos.map((dto) => new PerpInfo(dto));
|
||||
static fromDto(dto) {
|
||||
return new HealthCache(
|
||||
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
|
||||
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
|
||||
dto.perpInfos.map((dto) => new PerpInfo(dto)),
|
||||
);
|
||||
}
|
||||
|
||||
public health(healthType: HealthType): I80F48 {
|
||||
let health = ZERO_I80F48;
|
||||
const health = ZERO_I80F48();
|
||||
for (const tokenInfo of this.tokenInfos) {
|
||||
const contrib = tokenInfo.healthContribution(healthType);
|
||||
health = health.add(contrib);
|
||||
health.iadd(contrib);
|
||||
}
|
||||
for (const serum3Info of this.serum3Infos) {
|
||||
const contrib = serum3Info.healthContribution(
|
||||
healthType,
|
||||
this.tokenInfos,
|
||||
);
|
||||
health = health.add(contrib);
|
||||
health.iadd(contrib);
|
||||
}
|
||||
for (const perpInfo of this.perpInfos) {
|
||||
const contrib = perpInfo.healthContribution(healthType);
|
||||
health = health.add(contrib);
|
||||
health.iadd(contrib);
|
||||
}
|
||||
return health;
|
||||
}
|
||||
|
||||
public assets(healthType: HealthType): I80F48 {
|
||||
let assets = ZERO_I80F48;
|
||||
const assets = ZERO_I80F48();
|
||||
for (const tokenInfo of this.tokenInfos) {
|
||||
const contrib = tokenInfo.healthContribution(healthType);
|
||||
if (contrib.isPos()) {
|
||||
assets = assets.add(contrib);
|
||||
assets.iadd(contrib);
|
||||
}
|
||||
}
|
||||
for (const serum3Info of this.serum3Infos) {
|
||||
|
@ -81,24 +86,24 @@ export class HealthCache {
|
|||
this.tokenInfos,
|
||||
);
|
||||
if (contrib.isPos()) {
|
||||
assets = assets.add(contrib);
|
||||
assets.iadd(contrib);
|
||||
}
|
||||
}
|
||||
for (const perpInfo of this.perpInfos) {
|
||||
const contrib = perpInfo.healthContribution(healthType);
|
||||
if (contrib.isPos()) {
|
||||
assets = assets.add(contrib);
|
||||
assets.iadd(contrib);
|
||||
}
|
||||
}
|
||||
return assets;
|
||||
}
|
||||
|
||||
public liabs(healthType: HealthType): I80F48 {
|
||||
let liabs = ZERO_I80F48;
|
||||
const liabs = ZERO_I80F48();
|
||||
for (const tokenInfo of this.tokenInfos) {
|
||||
const contrib = tokenInfo.healthContribution(healthType);
|
||||
if (contrib.isNeg()) {
|
||||
liabs = liabs.sub(contrib);
|
||||
liabs.isub(contrib);
|
||||
}
|
||||
}
|
||||
for (const serum3Info of this.serum3Infos) {
|
||||
|
@ -107,28 +112,28 @@ export class HealthCache {
|
|||
this.tokenInfos,
|
||||
);
|
||||
if (contrib.isNeg()) {
|
||||
liabs = liabs.sub(contrib);
|
||||
liabs.isub(contrib);
|
||||
}
|
||||
}
|
||||
for (const perpInfo of this.perpInfos) {
|
||||
const contrib = perpInfo.healthContribution(healthType);
|
||||
if (contrib.isNeg()) {
|
||||
liabs = liabs.sub(contrib);
|
||||
liabs.isub(contrib);
|
||||
}
|
||||
}
|
||||
return liabs;
|
||||
}
|
||||
|
||||
public healthRatio(healthType: HealthType): I80F48 {
|
||||
let assets = ZERO_I80F48;
|
||||
let liabs = ZERO_I80F48;
|
||||
const assets = ZERO_I80F48();
|
||||
const liabs = ZERO_I80F48();
|
||||
|
||||
for (const tokenInfo of this.tokenInfos) {
|
||||
const contrib = tokenInfo.healthContribution(healthType);
|
||||
if (contrib.isPos()) {
|
||||
assets = assets.add(contrib);
|
||||
assets.iadd(contrib);
|
||||
} else {
|
||||
liabs = liabs.sub(contrib);
|
||||
liabs.isub(contrib);
|
||||
}
|
||||
}
|
||||
for (const serum3Info of this.serum3Infos) {
|
||||
|
@ -137,24 +142,24 @@ export class HealthCache {
|
|||
this.tokenInfos,
|
||||
);
|
||||
if (contrib.isPos()) {
|
||||
assets = assets.add(contrib);
|
||||
assets.iadd(contrib);
|
||||
} else {
|
||||
liabs = liabs.sub(contrib);
|
||||
liabs.isub(contrib);
|
||||
}
|
||||
}
|
||||
for (const perpInfo of this.perpInfos) {
|
||||
const contrib = perpInfo.healthContribution(healthType);
|
||||
if (contrib.isPos()) {
|
||||
assets = assets.add(contrib);
|
||||
assets.iadd(contrib);
|
||||
} else {
|
||||
liabs = liabs.sub(contrib);
|
||||
liabs.isub(contrib);
|
||||
}
|
||||
}
|
||||
|
||||
if (liabs.isPos()) {
|
||||
return HUNDRED_I80F48.mul(assets.sub(liabs).div(liabs));
|
||||
return HUNDRED_I80F48().mul(assets.sub(liabs).div(liabs));
|
||||
} else {
|
||||
return MAX_I80F48;
|
||||
return MAX_I80F48();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,10 +177,50 @@ export class HealthCache {
|
|||
return this.findTokenInfoIndex(bank.tokenIndex);
|
||||
}
|
||||
|
||||
private static logHealthCache(debug: string, healthCache: HealthCache) {
|
||||
console.log(debug);
|
||||
adjustSerum3Reserved(
|
||||
// todo change indices to types from numbers
|
||||
marketIndex: number,
|
||||
baseTokenIndex: number,
|
||||
reservedBaseChange: I80F48,
|
||||
freeBaseChange: I80F48,
|
||||
quoteTokenIndex: number,
|
||||
reservedQuoteChange: I80F48,
|
||||
freeQuoteChange: I80F48,
|
||||
) {
|
||||
const baseEntryIndex = this.findTokenInfoIndex(baseTokenIndex);
|
||||
const quoteEntryIndex = this.findTokenInfoIndex(quoteTokenIndex);
|
||||
|
||||
const baseEntry = this.tokenInfos[baseEntryIndex];
|
||||
const reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice);
|
||||
|
||||
const quoteEntry = this.tokenInfos[quoteEntryIndex];
|
||||
reservedAmount.iadd(reservedQuoteChange.mul(quoteEntry.oraclePrice));
|
||||
|
||||
// Apply it to the tokens
|
||||
baseEntry.serum3MaxReserved.iadd(reservedAmount);
|
||||
baseEntry.balance.iadd(freeBaseChange.mul(baseEntry.oraclePrice));
|
||||
quoteEntry.serum3MaxReserved.iadd(reservedAmount);
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
|
||||
}
|
||||
|
||||
public static logHealthCache(debug: string, healthCache: HealthCache) {
|
||||
if (debug) console.log(debug);
|
||||
for (const token of healthCache.tokenInfos) {
|
||||
console.log(`${token.toString()}`);
|
||||
console.log(` ${token.toString()}`);
|
||||
}
|
||||
for (const serum3Info of healthCache.serum3Infos) {
|
||||
console.log(` ${serum3Info.toString(healthCache.tokenInfos)}`);
|
||||
}
|
||||
console.log(
|
||||
` assets ${healthCache.assets(
|
||||
|
@ -209,14 +254,132 @@ export class HealthCache {
|
|||
throw new Error(
|
||||
`Oracle price not loaded for ${change.mintPk.toString()}`,
|
||||
);
|
||||
adjustedCache.tokenInfos[changeIndex].balance = adjustedCache.tokenInfos[
|
||||
changeIndex
|
||||
].balance.add(change.nativeTokenAmount.mul(bank.price));
|
||||
adjustedCache.tokenInfos[changeIndex].balance.iadd(
|
||||
change.nativeTokenAmount.mul(bank.price),
|
||||
);
|
||||
}
|
||||
// HealthCache.logHealthCache('afterChange', adjustedCache);
|
||||
return adjustedCache.healthRatio(healthType);
|
||||
}
|
||||
|
||||
simHealthRatioWithSerum3BidChanges(
|
||||
group: Group,
|
||||
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];
|
||||
|
||||
// Move token balance to reserved funds in open orders,
|
||||
// essentially simulating a place order
|
||||
|
||||
// Reduce token balance for quote
|
||||
adjustedCache.tokenInfos[quoteIndex].balance.isub(
|
||||
bidNativeQuoteAmount.mul(quote.oraclePrice),
|
||||
);
|
||||
|
||||
// Increase reserved in Serum3Info for quote
|
||||
adjustedCache.adjustSerum3Reserved(
|
||||
serum3Market.marketIndex,
|
||||
serum3Market.baseTokenIndex,
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
serum3Market.quoteTokenIndex,
|
||||
bidNativeQuoteAmount,
|
||||
ZERO_I80F48(),
|
||||
);
|
||||
return adjustedCache.healthRatio(healthType);
|
||||
}
|
||||
|
||||
simHealthRatioWithSerum3AskChanges(
|
||||
group: Group,
|
||||
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];
|
||||
|
||||
// Move token balance to reserved funds in open orders,
|
||||
// essentially simulating a place order
|
||||
|
||||
// Reduce token balance for base
|
||||
adjustedCache.tokenInfos[baseIndex].balance.isub(
|
||||
askNativeBaseAmount.mul(base.oraclePrice),
|
||||
);
|
||||
|
||||
// Increase reserved in Serum3Info for base
|
||||
adjustedCache.adjustSerum3Reserved(
|
||||
serum3Market.marketIndex,
|
||||
serum3Market.baseTokenIndex,
|
||||
askNativeBaseAmount,
|
||||
ZERO_I80F48(),
|
||||
serum3Market.quoteTokenIndex,
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
);
|
||||
return adjustedCache.healthRatio(healthType);
|
||||
}
|
||||
|
||||
private static binaryApproximationSearch(
|
||||
left: I80F48,
|
||||
leftRatio: I80F48,
|
||||
right: I80F48,
|
||||
rightRatio: I80F48,
|
||||
targetRatio: I80F48,
|
||||
healthRatioAfterActionFn: (I80F48) => I80F48,
|
||||
) {
|
||||
const maxIterations = 40;
|
||||
// TODO: make relative to health ratio decimals? Might be over engineering
|
||||
const targetError = I80F48.fromNumber(0.001);
|
||||
|
||||
if (
|
||||
(leftRatio.sub(targetRatio).isPos() &&
|
||||
rightRatio.sub(targetRatio).isPos()) ||
|
||||
(leftRatio.sub(targetRatio).isNeg() &&
|
||||
rightRatio.sub(targetRatio).isNeg())
|
||||
) {
|
||||
throw new Error(
|
||||
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
|
||||
);
|
||||
}
|
||||
|
||||
let newAmount;
|
||||
for (const key of Array(maxIterations).fill(0).keys()) {
|
||||
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
|
||||
const newAmountRatio = healthRatioAfterActionFn(newAmount);
|
||||
const error = newAmountRatio.sub(targetRatio);
|
||||
if (error.isPos() && error.lt(targetError)) {
|
||||
return newAmount;
|
||||
}
|
||||
if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) {
|
||||
left = newAmount;
|
||||
} else {
|
||||
right = newAmount;
|
||||
rightRatio = newAmountRatio;
|
||||
}
|
||||
}
|
||||
console.error(
|
||||
`Unable to get targetRatio within ${maxIterations} iterations`,
|
||||
);
|
||||
return newAmount;
|
||||
}
|
||||
|
||||
getMaxSourceForTokenSwap(
|
||||
group: Group,
|
||||
sourceMintPk: PublicKey,
|
||||
|
@ -227,20 +390,20 @@ export class HealthCache {
|
|||
const targetBank: Bank = group.getFirstBankByMint(targetMintPk);
|
||||
|
||||
if (sourceMintPk.equals(targetMintPk)) {
|
||||
return ZERO_I80F48;
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
if (!sourceBank.price || sourceBank.price.lte(ZERO_I80F48)) {
|
||||
return ZERO_I80F48;
|
||||
if (!sourceBank.price || sourceBank.price.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
if (
|
||||
sourceBank.initLiabWeight
|
||||
.sub(targetBank.initAssetWeight)
|
||||
.abs()
|
||||
.lte(ZERO_I80F48)
|
||||
.lte(ZERO_I80F48())
|
||||
) {
|
||||
return ZERO_I80F48;
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
// The health_ratio is a nonlinear based on swap amount.
|
||||
|
@ -252,8 +415,8 @@ export class HealthCache {
|
|||
// - be careful about finding the minRatio point: the function isn't convex
|
||||
|
||||
const initialRatio = this.healthRatio(HealthType.init);
|
||||
if (initialRatio.lte(ZERO_I80F48)) {
|
||||
return ZERO_I80F48;
|
||||
if (initialRatio.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
const healthCacheClone: HealthCache = _.cloneDeep(this);
|
||||
|
@ -271,10 +434,8 @@ export class HealthCache {
|
|||
function cacheAfterSwap(amount: I80F48) {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
|
||||
// HealthCache.logHealthCache('beforeSwap', adjustedCache);
|
||||
adjustedCache.tokenInfos[sourceIndex].balance =
|
||||
adjustedCache.tokenInfos[sourceIndex].balance.sub(amount);
|
||||
adjustedCache.tokenInfos[targetIndex].balance =
|
||||
adjustedCache.tokenInfos[targetIndex].balance.add(amount);
|
||||
adjustedCache.tokenInfos[sourceIndex].balance.isub(amount);
|
||||
adjustedCache.tokenInfos[targetIndex].balance.iadd(amount);
|
||||
// HealthCache.logHealthCache('afterSwap', adjustedCache);
|
||||
return adjustedCache;
|
||||
}
|
||||
|
@ -285,60 +446,16 @@ export class HealthCache {
|
|||
|
||||
const point0Amount = source.balance
|
||||
.min(target.balance.neg())
|
||||
.max(ZERO_I80F48);
|
||||
.max(ZERO_I80F48());
|
||||
const point1Amount = source.balance
|
||||
.max(target.balance.neg())
|
||||
.max(ZERO_I80F48);
|
||||
.max(ZERO_I80F48());
|
||||
const cache0 = cacheAfterSwap(point0Amount);
|
||||
const point0Ratio = cache0.healthRatio(HealthType.init);
|
||||
const point0Health = cache0.health(HealthType.init);
|
||||
const cache1 = cacheAfterSwap(point1Amount);
|
||||
const point1Ratio = cache1.healthRatio(HealthType.init);
|
||||
const point1Health = cache1.health(HealthType.init);
|
||||
|
||||
function binaryApproximationSearch(
|
||||
left: I80F48,
|
||||
leftRatio: I80F48,
|
||||
right: I80F48,
|
||||
rightRatio: I80F48,
|
||||
targetRatio: I80F48,
|
||||
) {
|
||||
const maxIterations = 20;
|
||||
// TODO: make relative to health ratio decimals? Might be over engineering
|
||||
const targetError = I80F48.fromString('0.001');
|
||||
|
||||
if (
|
||||
(leftRatio.sub(targetRatio).isPos() &&
|
||||
rightRatio.sub(targetRatio).isPos()) ||
|
||||
(leftRatio.sub(targetRatio).isNeg() &&
|
||||
rightRatio.sub(targetRatio).isNeg())
|
||||
) {
|
||||
throw new Error(
|
||||
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
|
||||
);
|
||||
}
|
||||
|
||||
let newAmount;
|
||||
for (const key of Array(maxIterations).fill(0).keys()) {
|
||||
newAmount = left.add(right).mul(I80F48.fromString('0.5'));
|
||||
const newAmountRatio = healthRatioAfterSwap(newAmount);
|
||||
const error = newAmountRatio.sub(targetRatio);
|
||||
if (error.isPos() && error.lt(targetError)) {
|
||||
return newAmount;
|
||||
}
|
||||
if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) {
|
||||
left = newAmount;
|
||||
} else {
|
||||
right = newAmount;
|
||||
rightRatio = newAmountRatio;
|
||||
}
|
||||
}
|
||||
console.error(
|
||||
`Unable to get targetRatio within ${maxIterations} iterations`,
|
||||
);
|
||||
return newAmount;
|
||||
}
|
||||
|
||||
let amount: I80F48;
|
||||
|
||||
if (
|
||||
|
@ -356,43 +473,37 @@ export class HealthCache {
|
|||
} else if (point1Ratio.gt(initialRatio)) {
|
||||
amount = point1Amount;
|
||||
} else {
|
||||
amount = ZERO_I80F48;
|
||||
amount = ZERO_I80F48();
|
||||
}
|
||||
} else if (point1Ratio.gte(minRatio)) {
|
||||
// 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.
|
||||
if (point1Health.lte(ZERO_I80F48)) {
|
||||
return ZERO_I80F48;
|
||||
if (point1Health.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
const zeroHealthAmount = point1Amount.add(
|
||||
point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)),
|
||||
);
|
||||
// console.log(`point1Amount ${point1Amount}`);
|
||||
// console.log(`point1Health ${point1Health}`);
|
||||
// console.log(`point1Ratio ${point1Ratio}`);
|
||||
// console.log(`point0Amount ${point0Amount}`);
|
||||
// console.log(`point0Health ${point0Health}`);
|
||||
// console.log(`point0Ratio ${point0Ratio}`);
|
||||
// console.log(`zeroHealthAmount ${zeroHealthAmount}`);
|
||||
const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount);
|
||||
// console.log(`zeroHealthRatio ${zeroHealthRatio}`);
|
||||
amount = binaryApproximationSearch(
|
||||
amount = HealthCache.binaryApproximationSearch(
|
||||
point1Amount,
|
||||
point1Ratio,
|
||||
zeroHealthAmount,
|
||||
zeroHealthRatio,
|
||||
minRatio,
|
||||
healthRatioAfterSwap,
|
||||
);
|
||||
} else if (point0Ratio.gte(minRatio)) {
|
||||
// Must be between point0Amount and point1Amount.
|
||||
amount = binaryApproximationSearch(
|
||||
amount = HealthCache.binaryApproximationSearch(
|
||||
point0Amount,
|
||||
point0Ratio,
|
||||
point1Amount,
|
||||
point1Ratio,
|
||||
minRatio,
|
||||
healthRatioAfterSwap,
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
|
@ -403,11 +514,131 @@ export class HealthCache {
|
|||
return amount
|
||||
.div(source.oraclePrice)
|
||||
.div(
|
||||
ONE_I80F48.add(
|
||||
ONE_I80F48().add(
|
||||
group.getFirstBankByMint(sourceMintPk).loanOriginationFeeRate,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getMaxForSerum3Order(
|
||||
group: Group,
|
||||
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}`);
|
||||
}
|
||||
|
||||
const healthCacheClone: HealthCache = _.cloneDeep(this);
|
||||
|
||||
const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank);
|
||||
const quoteIndex = healthCacheClone.getOrCreateTokenInfoIndex(quoteBank);
|
||||
const base = healthCacheClone.tokenInfos[baseIndex];
|
||||
const quote = healthCacheClone.tokenInfos[quoteIndex];
|
||||
|
||||
// Binary search between current health (0 sized new order) and
|
||||
// an amount to trade which will bring health to 0.
|
||||
|
||||
// Current health and amount i.e. 0
|
||||
const initialAmount = ZERO_I80F48();
|
||||
const initialHealth = this.health(HealthType.init);
|
||||
const initialRatio = this.healthRatio(HealthType.init);
|
||||
if (initialRatio.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
// Amount which would bring health to 0
|
||||
// amount = max(A_deposits, B_borrows) + init_health / (A_liab_weight - B_asset_weight)
|
||||
// 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
|
||||
let zeroAmount;
|
||||
if (side == Serum3Side.ask) {
|
||||
const quoteBorrows = quote.balance.lt(ZERO_I80F48())
|
||||
? quote.balance.abs()
|
||||
: ZERO_I80F48();
|
||||
zeroAmount = base.balance
|
||||
.max(quoteBorrows)
|
||||
.add(
|
||||
initialHealth.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(
|
||||
quote
|
||||
.liabWeight(HealthType.init)
|
||||
.sub(base.assetWeight(HealthType.init)),
|
||||
),
|
||||
);
|
||||
}
|
||||
const cache = cacheAfterPlacingOrder(zeroAmount);
|
||||
const zeroAmountRatio = cache.healthRatio(HealthType.init);
|
||||
|
||||
function cacheAfterPlacingOrder(amount: I80F48) {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
|
||||
|
||||
side === Serum3Side.ask
|
||||
? adjustedCache.tokenInfos[baseIndex].balance.isub(amount)
|
||||
: adjustedCache.tokenInfos[quoteIndex].balance.isub(amount);
|
||||
|
||||
adjustedCache.adjustSerum3Reserved(
|
||||
serum3Market.marketIndex,
|
||||
serum3Market.baseTokenIndex,
|
||||
side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
serum3Market.quoteTokenIndex,
|
||||
side === Serum3Side.bid ? amount.div(quote.oraclePrice) : ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
);
|
||||
|
||||
return adjustedCache;
|
||||
}
|
||||
|
||||
function healthRatioAfterPlacingOrder(amount: I80F48): I80F48 {
|
||||
return cacheAfterPlacingOrder(amount).healthRatio(HealthType.init);
|
||||
}
|
||||
|
||||
const amount = HealthCache.binaryApproximationSearch(
|
||||
initialAmount,
|
||||
initialRatio,
|
||||
zeroAmount,
|
||||
zeroAmountRatio,
|
||||
minRatio,
|
||||
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))));
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenInfo {
|
||||
|
@ -450,8 +681,8 @@ export class TokenInfo {
|
|||
bank.maintLiabWeight,
|
||||
bank.initLiabWeight,
|
||||
bank.price,
|
||||
ZERO_I80F48,
|
||||
ZERO_I80F48,
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -476,20 +707,30 @@ export class TokenInfo {
|
|||
}
|
||||
|
||||
toString() {
|
||||
return ` tokenIndex: ${this.tokenIndex}, balance: ${this.balance}`;
|
||||
return ` tokenIndex: ${this.tokenIndex}, balance: ${
|
||||
this.balance
|
||||
}, serum3MaxReserved: ${
|
||||
this.serum3MaxReserved
|
||||
}, initHealth ${this.healthContribution(HealthType.init)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class Serum3Info {
|
||||
constructor(dto: Serum3InfoDto) {
|
||||
this.reserved = I80F48.from(dto.reserved);
|
||||
this.baseIndex = dto.baseIndex;
|
||||
this.quoteIndex = dto.quoteIndex;
|
||||
}
|
||||
constructor(
|
||||
public reserved: I80F48,
|
||||
public baseIndex: number,
|
||||
public quoteIndex: number,
|
||||
public marketIndex: number,
|
||||
) {}
|
||||
|
||||
reserved: I80F48;
|
||||
baseIndex: number;
|
||||
quoteIndex: number;
|
||||
static fromDto(dto: Serum3InfoDto) {
|
||||
return new Serum3Info(
|
||||
I80F48.from(dto.reserved),
|
||||
dto.baseIndex,
|
||||
dto.quoteIndex,
|
||||
dto.marketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
|
||||
const baseInfo = tokenInfos[this.baseIndex];
|
||||
|
@ -497,7 +738,7 @@ export class Serum3Info {
|
|||
const reserved = this.reserved;
|
||||
|
||||
if (reserved.isZero()) {
|
||||
return ZERO_I80F48;
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
// How much the health would increase if the reserved balance were applied to the passed
|
||||
|
@ -512,15 +753,14 @@ export class Serum3Info {
|
|||
let assetPart, liabPart;
|
||||
if (maxBalance.gte(reserved)) {
|
||||
assetPart = reserved;
|
||||
liabPart = ZERO_I80F48;
|
||||
liabPart = ZERO_I80F48();
|
||||
} else if (maxBalance.isNeg()) {
|
||||
assetPart = ZERO_I80F48;
|
||||
assetPart = ZERO_I80F48();
|
||||
liabPart = reserved;
|
||||
} else {
|
||||
assetPart = maxBalance;
|
||||
liabPart = reserved.sub(maxBalance);
|
||||
}
|
||||
|
||||
const assetWeight = tokenInfo.assetWeight(healthType);
|
||||
const liabWeight = tokenInfo.liabWeight(healthType);
|
||||
return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
|
||||
|
@ -530,6 +770,14 @@ export class Serum3Info {
|
|||
const reservedAsQuote = computeHealthEffect(quoteInfo);
|
||||
return reservedAsBase.min(reservedAsQuote);
|
||||
}
|
||||
|
||||
toString(tokenInfos: TokenInfo[]) {
|
||||
return ` marketIndex: ${this.marketIndex}, baseIndex: ${
|
||||
this.baseIndex
|
||||
}, quoteIndex: ${this.quoteIndex}, reserved: ${
|
||||
this.reserved
|
||||
}, initHealth ${this.healthContribution(HealthType.init, tokenInfos)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class PerpInfo {
|
||||
|
@ -566,7 +814,7 @@ export class PerpInfo {
|
|||
|
||||
// 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);
|
||||
return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -586,12 +834,39 @@ export class TokenInfoDto {
|
|||
balance: I80F48Dto;
|
||||
// in health-reference-token native units
|
||||
serum3MaxReserved: I80F48Dto;
|
||||
|
||||
constructor(
|
||||
tokenIndex: number,
|
||||
maintAssetWeight: I80F48Dto,
|
||||
initAssetWeight: I80F48Dto,
|
||||
maintLiabWeight: I80F48Dto,
|
||||
initLiabWeight: I80F48Dto,
|
||||
oraclePrice: I80F48Dto,
|
||||
balance: I80F48Dto,
|
||||
serum3MaxReserved: I80F48Dto,
|
||||
) {
|
||||
this.tokenIndex = tokenIndex;
|
||||
this.maintAssetWeight = maintAssetWeight;
|
||||
this.initAssetWeight = initAssetWeight;
|
||||
this.maintLiabWeight = maintLiabWeight;
|
||||
this.initLiabWeight = initLiabWeight;
|
||||
this.oraclePrice = oraclePrice;
|
||||
this.balance = balance;
|
||||
this.serum3MaxReserved = serum3MaxReserved;
|
||||
}
|
||||
}
|
||||
|
||||
export class Serum3InfoDto {
|
||||
reserved: I80F48Dto;
|
||||
baseIndex: number;
|
||||
quoteIndex: number;
|
||||
marketIndex: number;
|
||||
|
||||
constructor(reserved: I80F48Dto, baseIndex: number, quoteIndex: number) {
|
||||
this.reserved = reserved;
|
||||
this.baseIndex = baseIndex;
|
||||
this.quoteIndex = quoteIndex;
|
||||
}
|
||||
}
|
||||
|
||||
export class PerpInfoDto {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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 { PublicKey } from '@solana/web3.js';
|
||||
import { MangoClient } from '../client';
|
||||
import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils';
|
||||
|
@ -7,6 +8,7 @@ import { Bank } from './bank';
|
|||
import { Group } from './group';
|
||||
import { HealthCache, HealthCacheDto } from './healthCache';
|
||||
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
|
||||
import { Serum3Market, Serum3Side } from './serum3';
|
||||
export class MangoAccount {
|
||||
public tokens: TokenPosition[];
|
||||
public serum3: Serum3Orders[];
|
||||
|
@ -91,6 +93,18 @@ export class MangoAccount {
|
|||
return this;
|
||||
}
|
||||
|
||||
tokensActive(): TokenPosition[] {
|
||||
return this.tokens.filter((token) => token.isActive());
|
||||
}
|
||||
|
||||
serum3Active(): Serum3Orders[] {
|
||||
return this.serum3.filter((serum3) => serum3.isActive());
|
||||
}
|
||||
|
||||
perpActive(): PerpPosition[] {
|
||||
return this.perps.filter((perp) => perp.isActive());
|
||||
}
|
||||
|
||||
findToken(tokenIndex: number): TokenPosition | undefined {
|
||||
return this.tokens.find((ta) => ta.tokenIndex == tokenIndex);
|
||||
}
|
||||
|
@ -112,7 +126,7 @@ export class MangoAccount {
|
|||
*/
|
||||
getTokenBalance(bank: Bank): I80F48 {
|
||||
const tp = this.findToken(bank.tokenIndex);
|
||||
return tp ? tp.balance(bank) : ZERO_I80F48;
|
||||
return tp ? tp.balance(bank) : ZERO_I80F48();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -122,7 +136,7 @@ export class MangoAccount {
|
|||
*/
|
||||
getTokenDeposits(bank: Bank): I80F48 {
|
||||
const tp = this.findToken(bank.tokenIndex);
|
||||
return tp ? tp.deposits(bank) : ZERO_I80F48;
|
||||
return tp ? tp.deposits(bank) : ZERO_I80F48();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,7 +146,7 @@ export class MangoAccount {
|
|||
*/
|
||||
getTokenBorrows(bank: Bank): I80F48 {
|
||||
const tp = this.findToken(bank.tokenIndex);
|
||||
return tp ? tp.borrows(bank) : ZERO_I80F48;
|
||||
return tp ? tp.borrows(bank) : ZERO_I80F48();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -209,7 +223,7 @@ export class MangoAccount {
|
|||
const equity = this.accountData.equity;
|
||||
const total_equity = equity.tokens.reduce(
|
||||
(a, b) => a.add(b.value),
|
||||
ZERO_I80F48,
|
||||
ZERO_I80F48(),
|
||||
);
|
||||
return total_equity;
|
||||
}
|
||||
|
@ -255,8 +269,8 @@ export class MangoAccount {
|
|||
|
||||
// Case 1:
|
||||
// Cannot withdraw if init health is below 0
|
||||
if (initHealth.lte(ZERO_I80F48)) {
|
||||
return ZERO_I80F48;
|
||||
if (initHealth.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
// Deposits need special treatment since they would neither count towards liabilities
|
||||
|
@ -264,12 +278,12 @@ export class MangoAccount {
|
|||
|
||||
const tp = this.findToken(tokenBank.tokenIndex);
|
||||
if (!tokenBank.price) return undefined;
|
||||
const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48;
|
||||
let existingPositionHealthContrib = ZERO_I80F48;
|
||||
if (existingTokenDeposits.gt(ZERO_I80F48)) {
|
||||
existingPositionHealthContrib = existingTokenDeposits
|
||||
.mul(tokenBank.price)
|
||||
.mul(tokenBank.initAssetWeight);
|
||||
const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48();
|
||||
const existingPositionHealthContrib = ZERO_I80F48();
|
||||
if (existingTokenDeposits.gt(ZERO_I80F48())) {
|
||||
existingTokenDeposits
|
||||
.imul(tokenBank.price)
|
||||
.imul(tokenBank.initAssetWeight);
|
||||
}
|
||||
|
||||
// Case 2: token deposits have higher contribution than initHealth,
|
||||
|
@ -296,7 +310,7 @@ export class MangoAccount {
|
|||
.div(tokenBank.initLiabWeight)
|
||||
.div(tokenBank.price);
|
||||
const maxBorrowNativeWithoutFees = maxBorrowNative.div(
|
||||
ONE_I80F48.add(tokenBank.loanOriginationFeeRate),
|
||||
ONE_I80F48().add(tokenBank.loanOriginationFeeRate),
|
||||
);
|
||||
// console.log(`initHealth ${initHealth}`);
|
||||
// console.log(
|
||||
|
@ -344,7 +358,7 @@ export class MangoAccount {
|
|||
group,
|
||||
sourceMintPk,
|
||||
targetMintPk,
|
||||
ONE_I80F48, // target 1% health
|
||||
ONE_I80F48(), // target 1% health
|
||||
)
|
||||
.mul(I80F48.fromNumber(slippageAndFeesFactor));
|
||||
}
|
||||
|
@ -431,25 +445,234 @@ export class MangoAccount {
|
|||
.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* The remaining native quote margin available for given market.
|
||||
*
|
||||
* TODO: this is a very bad estimation atm.
|
||||
* It assumes quote asset is always quote,
|
||||
* it assumes that there are no interaction effects,
|
||||
* it assumes that there are no existing borrows for either of the tokens in the market.
|
||||
*/
|
||||
getSerum3MarketMarginAvailable(
|
||||
public async loadSerum3OpenOrdersForMarket(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
marketName: string,
|
||||
): I80F48 | undefined {
|
||||
if (!this.accountData) return undefined;
|
||||
const initHealth = this.accountData.initHealth;
|
||||
const serum3Market = group.serum3MarketsMap.get(marketName)!;
|
||||
const marketAssetWeight = group.getFirstBankByTokenIndex(
|
||||
serum3Market.baseTokenIndex,
|
||||
).initAssetWeight;
|
||||
return initHealth.div(ONE_I80F48.sub(marketAssetWeight));
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<Order[]> {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
const serum3OO = this.serum3Active().find(
|
||||
(s) => s.marketIndex === serum3Market.marketIndex,
|
||||
);
|
||||
if (!serum3OO) {
|
||||
throw new Error(`No open orders account found for ${externalMarketPk}`);
|
||||
}
|
||||
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const [bidsInfo, asksInfo] =
|
||||
await client.program.provider.connection.getMultipleAccountsInfo([
|
||||
serum3MarketExternal.bidsAddress,
|
||||
serum3MarketExternal.asksAddress,
|
||||
]);
|
||||
if (!bidsInfo || !asksInfo) {
|
||||
throw new Error(
|
||||
`bids and asks ai were not fetched for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
const bids = Orderbook.decode(serum3MarketExternal, bidsInfo.data);
|
||||
const asks = Orderbook.decode(serum3MarketExternal, asksInfo.data);
|
||||
return [...bids, ...asks].filter((o) =>
|
||||
o.openOrdersAddress.equals(serum3OO.openOrders),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @param serum3Market
|
||||
* @returns maximum native 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(3),
|
||||
);
|
||||
}
|
||||
|
||||
public getMaxQuoteForSerum3BidUi(
|
||||
group: Group,
|
||||
externalMarketPk: PublicKey,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
const nativeAmount = this.getMaxQuoteForSerum3Bid(group, serum3Market);
|
||||
return toUiDecimals(
|
||||
nativeAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @param serum3Market
|
||||
* @returns maximum native 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(3),
|
||||
);
|
||||
}
|
||||
|
||||
public getMaxBaseForSerum3AskUi(
|
||||
group: Group,
|
||||
externalMarketPk: PublicKey,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
const nativeAmount = this.getMaxBaseForSerum3Ask(group, serum3Market);
|
||||
return toUiDecimals(
|
||||
nativeAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @param nativeQuoteAmount
|
||||
* @param serum3Market
|
||||
* @param healthType
|
||||
* @returns health ratio after a bid with nativeQuoteAmount 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(
|
||||
group: Group,
|
||||
uiQuoteAmount: number,
|
||||
externalMarketPk: PublicKey,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`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,
|
||||
);
|
||||
}
|
||||
|
||||
simHealthRatioWithSerum3AskUiChanges(
|
||||
group: Group,
|
||||
uiBaseAmount: number,
|
||||
externalMarketPk: PublicKey,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
return this.simHealthRatioWithSerum3AskChanges(
|
||||
group,
|
||||
toNative(
|
||||
uiBaseAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
|
||||
.mintDecimals,
|
||||
),
|
||||
serum3Market,
|
||||
healthType,
|
||||
).toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -468,19 +691,7 @@ export class MangoAccount {
|
|||
const initHealth = this.accountData.initHealth;
|
||||
const perpMarket = group.perpMarketsMap.get(marketName)!;
|
||||
const marketAssetWeight = perpMarket.initAssetWeight;
|
||||
return initHealth.div(ONE_I80F48.sub(marketAssetWeight));
|
||||
}
|
||||
|
||||
tokensActive(): TokenPosition[] {
|
||||
return this.tokens.filter((token) => token.isActive());
|
||||
}
|
||||
|
||||
serum3Active(): Serum3Orders[] {
|
||||
return this.serum3.filter((serum3) => serum3.isActive());
|
||||
}
|
||||
|
||||
perpActive(): PerpPosition[] {
|
||||
return this.perps.filter((perp) => perp.isActive());
|
||||
return initHealth.div(ONE_I80F48().sub(marketAssetWeight));
|
||||
}
|
||||
|
||||
toString(group?: Group): string {
|
||||
|
@ -558,8 +769,8 @@ export class TokenPosition {
|
|||
* @returns native deposits, 0 if position has borrows
|
||||
*/
|
||||
public deposits(bank: Bank): I80F48 {
|
||||
if (this.indexedPosition && this.indexedPosition.lt(ZERO_I80F48)) {
|
||||
return ZERO_I80F48;
|
||||
if (this.indexedPosition && this.indexedPosition.lt(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
return this.balance(bank);
|
||||
}
|
||||
|
@ -570,8 +781,8 @@ export class TokenPosition {
|
|||
* @returns native borrows, 0 if position has deposits
|
||||
*/
|
||||
public borrows(bank: Bank): I80F48 {
|
||||
if (this.indexedPosition && this.indexedPosition.gt(ZERO_I80F48)) {
|
||||
return ZERO_I80F48;
|
||||
if (this.indexedPosition && this.indexedPosition.gt(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
return this.balance(bank).abs();
|
||||
}
|
||||
|
@ -734,7 +945,7 @@ export class MangoAccountData {
|
|||
tokenAssets: any;
|
||||
}) {
|
||||
return new MangoAccountData(
|
||||
new HealthCache(event.healthCache),
|
||||
HealthCache.fromDto(event.healthCache),
|
||||
I80F48.from(event.initHealth),
|
||||
I80F48.from(event.maintHealth),
|
||||
Equity.from(event.equity),
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { Market, Orderbook } from '@project-serum/serum/lib/market';
|
||||
import { Cluster, PublicKey } from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
import { MangoClient } from '../client';
|
||||
import { SERUM3_PROGRAM_ID } from '../constants';
|
||||
import { Group } from './group';
|
||||
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48';
|
||||
|
||||
export class Serum3Market {
|
||||
public name: string;
|
||||
|
@ -44,6 +49,117 @@ export class Serum3Market {
|
|||
) {
|
||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @returns maximum leverage one can bid on this market, this is only for display purposes,
|
||||
* also see getMaxQuoteForSerum3BidUi and getMaxBaseForSerum3AskUi
|
||||
*/
|
||||
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())
|
||||
) {
|
||||
return MAX_I80F48().toNumber();
|
||||
}
|
||||
|
||||
return ONE_I80F48()
|
||||
.div(quoteBank.initLiabWeight.sub(baseBank.initAssetWeight))
|
||||
.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @returns maximum leverage one can ask on this market, this is only for display purposes,
|
||||
* also see getMaxQuoteForSerum3BidUi and getMaxBaseForSerum3AskUi
|
||||
*/
|
||||
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())
|
||||
) {
|
||||
return MAX_I80F48().toNumber();
|
||||
}
|
||||
|
||||
return ONE_I80F48()
|
||||
.div(baseBank.initLiabWeight.sub(quoteBank.initAssetWeight))
|
||||
.toNumber();
|
||||
}
|
||||
|
||||
public async loadBids(client: MangoClient, group: Group): Promise<Orderbook> {
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
this.serumMarketExternal.toBase58(),
|
||||
);
|
||||
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<Orderbook> {
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
this.serumMarketExternal.toBase58(),
|
||||
);
|
||||
if (!serum3MarketExternal) {
|
||||
throw new Error(
|
||||
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
|
||||
);
|
||||
}
|
||||
return await serum3MarketExternal.loadAsks(
|
||||
client.program.provider.connection,
|
||||
);
|
||||
}
|
||||
|
||||
public async logOb(client: MangoClient, group: Group): Promise<string> {
|
||||
let res = ``;
|
||||
res += ` ${this.name} OrderBook`;
|
||||
let orders = await this?.loadAsks(client, group);
|
||||
for (const order of orders!.items(true)) {
|
||||
res += `\n ${order.price.toString().padStart(10)}, ${order.size
|
||||
.toString()
|
||||
.padStart(10)}`;
|
||||
}
|
||||
res += `\n --------------------------`;
|
||||
orders = await this?.loadBids(client, group);
|
||||
for (const order of orders!.items(true)) {
|
||||
res += `\n ${order.price.toString().padStart(10)}, ${order.size
|
||||
.toString()
|
||||
.padStart(10)}`;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export class Serum3SelfTradeBehavior {
|
||||
|
@ -62,3 +178,21 @@ export class Serum3Side {
|
|||
static bid = { bid: {} };
|
||||
static ask = { ask: {} };
|
||||
}
|
||||
|
||||
export async function generateSerum3MarketExternalVaultSignerAddress(
|
||||
cluster: Cluster,
|
||||
serum3Market: Serum3Market,
|
||||
serum3MarketExternal: Market,
|
||||
): Promise<PublicKey> {
|
||||
return await PublicKey.createProgramAddress(
|
||||
[
|
||||
serum3Market.serumMarketExternal.toBuffer(),
|
||||
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
|
||||
Buffer,
|
||||
'le',
|
||||
8,
|
||||
),
|
||||
],
|
||||
SERUM3_PROGRAM_ID[cluster],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { AnchorProvider, BN, Program, Provider } from '@project-serum/anchor';
|
||||
import { getFeeRates, getFeeTier } from '@project-serum/serum';
|
||||
import { Order } from '@project-serum/serum/lib/market';
|
||||
import {
|
||||
closeAccount,
|
||||
initializeAccount,
|
||||
|
@ -37,6 +35,7 @@ import {
|
|||
import { StubOracle } from './accounts/oracle';
|
||||
import { OrderType, PerpMarket, Side } from './accounts/perp';
|
||||
import {
|
||||
generateSerum3MarketExternalVaultSignerAddress,
|
||||
Serum3Market,
|
||||
Serum3OrderType,
|
||||
Serum3SelfTradeBehavior,
|
||||
|
@ -348,7 +347,7 @@ export class MangoClient {
|
|||
}
|
||||
|
||||
return await this.program.methods
|
||||
.tokenDeregister(bank.tokenIndex)
|
||||
.tokenDeregister()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: adminPk,
|
||||
|
@ -681,9 +680,13 @@ export class MangoClient {
|
|||
mangoAccount: MangoAccount,
|
||||
): Promise<MangoAccountData | undefined> {
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
|
||||
mangoAccount,
|
||||
]);
|
||||
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;
|
||||
|
@ -785,6 +788,7 @@ export class MangoClient {
|
|||
group,
|
||||
[mangoAccount],
|
||||
[bank],
|
||||
[],
|
||||
);
|
||||
|
||||
const transaction = await this.program.methods
|
||||
|
@ -794,6 +798,7 @@ export class MangoClient {
|
|||
account: mangoAccount.publicKey,
|
||||
bank: bank.publicKey,
|
||||
vault: bank.vault,
|
||||
oracle: bank.oracle,
|
||||
tokenAccount: wrappedSolAccount?.publicKey ?? tokenAccountPk,
|
||||
tokenAuthority: mangoAccount.owner,
|
||||
})
|
||||
|
@ -858,6 +863,7 @@ export class MangoClient {
|
|||
group,
|
||||
[mangoAccount],
|
||||
[bank],
|
||||
[],
|
||||
);
|
||||
|
||||
const transaction = await this.program.methods
|
||||
|
@ -867,6 +873,7 @@ export class MangoClient {
|
|||
account: mangoAccount.publicKey,
|
||||
bank: bank.publicKey,
|
||||
vault: bank.vault,
|
||||
oracle: bank.oracle,
|
||||
tokenAccount: tokenAccountPk,
|
||||
owner: mangoAccount.owner,
|
||||
})
|
||||
|
@ -922,9 +929,11 @@ export class MangoClient {
|
|||
|
||||
public async serum3deregisterMarket(
|
||||
group: Group,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
return await this.program.methods
|
||||
.serum3DeregisterMarket()
|
||||
|
@ -986,7 +995,8 @@ export class MangoClient {
|
|||
mangoAccount: MangoAccount,
|
||||
marketName: string,
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market: Serum3Market = group.serum3MarketsMap.get(marketName)!;
|
||||
const serum3Market: Serum3Market =
|
||||
group.serum3MarketsMapByExternal.get(marketName)!;
|
||||
|
||||
return await this.program.methods
|
||||
.serum3CreateOpenOrders()
|
||||
|
@ -1005,9 +1015,11 @@ export class MangoClient {
|
|||
public async serum3CloseOpenOrders(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
const openOrders = mangoAccount.serum3.find(
|
||||
(account) => account.marketIndex === serum3Market.marketIndex,
|
||||
|
@ -1031,7 +1043,7 @@ export class MangoClient {
|
|||
public async serum3PlaceOrder(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
side: Serum3Side,
|
||||
price: number,
|
||||
size: number,
|
||||
|
@ -1040,46 +1052,48 @@ export class MangoClient {
|
|||
clientOrderId: number,
|
||||
limit: number,
|
||||
) {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
if (!mangoAccount.findSerum3Account(serum3Market.marketIndex)) {
|
||||
await this.serum3CreateOpenOrders(group, mangoAccount, 'BTC/USDC');
|
||||
mangoAccount = await this.getMangoAccount(mangoAccount);
|
||||
}
|
||||
|
||||
const serum3MarketExternal =
|
||||
group.serum3MarketExternalsMap.get(serum3MarketName)!;
|
||||
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const serum3MarketExternalVaultSigner =
|
||||
await PublicKey.createProgramAddress(
|
||||
[
|
||||
serum3Market.serumMarketExternal.toBuffer(),
|
||||
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
|
||||
Buffer,
|
||||
'le',
|
||||
8,
|
||||
),
|
||||
],
|
||||
SERUM3_PROGRAM_ID[this.cluster],
|
||||
await generateSerum3MarketExternalVaultSignerAddress(
|
||||
this.cluster,
|
||||
serum3Market,
|
||||
serum3MarketExternal,
|
||||
);
|
||||
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
|
||||
mangoAccount,
|
||||
]);
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
group,
|
||||
[mangoAccount],
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
const limitPrice = serum3MarketExternal.priceNumberToLots(price);
|
||||
const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size);
|
||||
const feeTier = getFeeTier(0, 0 /** TODO: fix msrm/srm balance */);
|
||||
const rates = getFeeRates(feeTier);
|
||||
const maxQuoteQuantity = new BN(
|
||||
serum3MarketExternal.decoded.quoteLotSize.toNumber() *
|
||||
(1 + rates.taker) /** TODO: fix taker/maker */,
|
||||
).mul(
|
||||
serum3MarketExternal
|
||||
.baseSizeNumberToLots(size)
|
||||
.mul(serum3MarketExternal.priceNumberToLots(price)),
|
||||
);
|
||||
const maxQuoteQuantity = serum3MarketExternal.decoded.quoteLotSize
|
||||
.mul(new BN(1 + group.getFeeRate(orderType === Serum3OrderType.postOnly)))
|
||||
.mul(
|
||||
serum3MarketExternal
|
||||
.baseSizeNumberToLots(size)
|
||||
.mul(serum3MarketExternal.priceNumberToLots(price)),
|
||||
);
|
||||
const payerTokenIndex = (() => {
|
||||
if (side == Serum3Side.bid) {
|
||||
return serum3Market.quoteTokenIndex;
|
||||
} else {
|
||||
return serum3Market.baseTokenIndex;
|
||||
}
|
||||
})();
|
||||
|
||||
return await this.program.methods
|
||||
.serum3PlaceOrder(
|
||||
|
@ -1108,14 +1122,8 @@ export class MangoClient {
|
|||
marketBaseVault: serum3MarketExternal.decoded.baseVault,
|
||||
marketQuoteVault: serum3MarketExternal.decoded.quoteVault,
|
||||
marketVaultSigner: serum3MarketExternalVaultSigner,
|
||||
quoteBank: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
|
||||
.publicKey,
|
||||
quoteVault: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
|
||||
.vault,
|
||||
baseBank: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
|
||||
.publicKey,
|
||||
baseVault: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
|
||||
.vault,
|
||||
payerBank: group.getFirstBankByTokenIndex(payerTokenIndex).publicKey,
|
||||
payerVault: group.getFirstBankByTokenIndex(payerTokenIndex).vault,
|
||||
})
|
||||
.remainingAccounts(
|
||||
healthRemainingAccounts.map(
|
||||
|
@ -1129,13 +1137,16 @@ export class MangoClient {
|
|||
async serum3CancelAllorders(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
limit: number,
|
||||
) {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
const serum3MarketExternal =
|
||||
group.serum3MarketExternalsMap.get(serum3MarketName)!;
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
return await this.program.methods
|
||||
.serum3CancelAllOrders(limit)
|
||||
|
@ -1158,25 +1169,19 @@ export class MangoClient {
|
|||
async serum3SettleFunds(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
|
||||
const serum3MarketExternal =
|
||||
group.serum3MarketExternalsMap.get(serum3MarketName)!;
|
||||
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const serum3MarketExternalVaultSigner =
|
||||
// TODO: put into a helper method, and remove copy pasta
|
||||
await PublicKey.createProgramAddress(
|
||||
[
|
||||
serum3Market.serumMarketExternal.toBuffer(),
|
||||
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
|
||||
Buffer,
|
||||
'le',
|
||||
8,
|
||||
),
|
||||
],
|
||||
SERUM3_PROGRAM_ID[this.cluster],
|
||||
await generateSerum3MarketExternalVaultSignerAddress(
|
||||
this.cluster,
|
||||
serum3Market,
|
||||
serum3MarketExternal,
|
||||
);
|
||||
|
||||
return await this.program.methods
|
||||
|
@ -1208,14 +1213,17 @@ export class MangoClient {
|
|||
async serum3CancelOrder(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
side: Serum3Side,
|
||||
orderId: BN,
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
const serum3MarketExternal =
|
||||
group.serum3MarketExternalsMap.get(serum3MarketName)!;
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
return await this.program.methods
|
||||
.serum3CancelOrder(side, orderId)
|
||||
|
@ -1234,20 +1242,6 @@ export class MangoClient {
|
|||
.rpc();
|
||||
}
|
||||
|
||||
async getSerum3Orders(
|
||||
group: Group,
|
||||
serum3MarketName: string,
|
||||
): Promise<Order[]> {
|
||||
const serum3MarketExternal =
|
||||
group.serum3MarketExternalsMap.get(serum3MarketName)!;
|
||||
|
||||
// TODO: filter for mango account
|
||||
return await serum3MarketExternal.loadOrdersForOwner(
|
||||
this.program.provider.connection,
|
||||
group.publicKey,
|
||||
);
|
||||
}
|
||||
|
||||
/// perps
|
||||
|
||||
async perpCreateMarket(
|
||||
|
@ -1470,9 +1464,13 @@ export class MangoClient {
|
|||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
|
||||
mangoAccount,
|
||||
]);
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
group,
|
||||
[mangoAccount],
|
||||
[],
|
||||
[perpMarket],
|
||||
);
|
||||
|
||||
const [nativePrice, nativeQuantity] = perpMarket.uiToNativePriceQuantity(
|
||||
price,
|
||||
|
@ -1543,6 +1541,7 @@ export class MangoClient {
|
|||
group,
|
||||
[mangoAccount],
|
||||
[inputBank, outputBank],
|
||||
[],
|
||||
);
|
||||
const parsedHealthAccounts = healthRemainingAccounts.map(
|
||||
(pk) =>
|
||||
|
@ -1727,6 +1726,7 @@ export class MangoClient {
|
|||
group,
|
||||
[liqor, liqee],
|
||||
[assetBank, liabBank],
|
||||
[],
|
||||
);
|
||||
|
||||
const parsedHealthAccounts = healthRemainingAccounts.map(
|
||||
|
@ -1802,31 +1802,39 @@ export class MangoClient {
|
|||
|
||||
/// private
|
||||
|
||||
// todo make private
|
||||
public buildHealthRemainingAccounts(
|
||||
retriever: AccountRetriever,
|
||||
group: Group,
|
||||
mangoAccounts: MangoAccount[],
|
||||
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
|
||||
banks: Bank[],
|
||||
perpMarkets: PerpMarket[],
|
||||
): PublicKey[] {
|
||||
if (retriever === AccountRetriever.Fixed) {
|
||||
return this.buildFixedAccountRetrieverHealthAccounts(
|
||||
group,
|
||||
mangoAccounts[0],
|
||||
banks,
|
||||
perpMarkets,
|
||||
);
|
||||
} else {
|
||||
return this.buildScanningAccountRetrieverHealthAccounts(
|
||||
group,
|
||||
mangoAccounts,
|
||||
banks,
|
||||
perpMarkets,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// todo make private
|
||||
public buildFixedAccountRetrieverHealthAccounts(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
|
||||
// Banks and perpMarkets for whom positions don't exist on mango account,
|
||||
// but user would potentially open new positions.
|
||||
banks: Bank[],
|
||||
perpMarkets: PerpMarket[],
|
||||
): PublicKey[] {
|
||||
const healthRemainingAccounts: PublicKey[] = [];
|
||||
|
||||
|
@ -1857,11 +1865,7 @@ export class MangoClient {
|
|||
healthRemainingAccounts.push(
|
||||
...mintInfos.map((mintInfo) => mintInfo.oracle),
|
||||
);
|
||||
healthRemainingAccounts.push(
|
||||
...mangoAccount.serum3
|
||||
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
|
||||
.map((serum3Account) => serum3Account.openOrders),
|
||||
);
|
||||
|
||||
healthRemainingAccounts.push(
|
||||
...mangoAccount.perps
|
||||
.filter((perp) => perp.marketIndex !== 65535)
|
||||
|
@ -1872,14 +1876,36 @@ export class MangoClient {
|
|||
)[0].publicKey,
|
||||
),
|
||||
);
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
healthRemainingAccounts.push(
|
||||
...mangoAccount.serum3
|
||||
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
|
||||
.map((serum3Account) => serum3Account.openOrders),
|
||||
);
|
||||
|
||||
// debugHealthAccounts(group, mangoAccount, healthRemainingAccounts);
|
||||
|
||||
return healthRemainingAccounts;
|
||||
}
|
||||
|
||||
// todo make private
|
||||
public buildScanningAccountRetrieverHealthAccounts(
|
||||
group: Group,
|
||||
mangoAccounts: MangoAccount[],
|
||||
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
|
||||
banks: Bank[],
|
||||
perpMarkets: PerpMarket[],
|
||||
): PublicKey[] {
|
||||
const healthRemainingAccounts: PublicKey[] = [];
|
||||
|
||||
|
@ -1907,6 +1933,7 @@ export class MangoClient {
|
|||
healthRemainingAccounts.push(
|
||||
...mintInfos.map((mintInfo) => mintInfo.oracle),
|
||||
);
|
||||
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
healthRemainingAccounts.push(
|
||||
...mangoAccount.serum3
|
||||
|
@ -1914,6 +1941,7 @@ export class MangoClient {
|
|||
.map((serum3Account) => serum3Account.openOrders),
|
||||
);
|
||||
}
|
||||
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
healthRemainingAccounts.push(
|
||||
...mangoAccount.perps
|
||||
|
@ -1926,6 +1954,20 @@ export class MangoClient {
|
|||
),
|
||||
);
|
||||
}
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return healthRemainingAccounts;
|
||||
}
|
||||
|
|
|
@ -1,38 +1,47 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { coder } from '@project-serum/anchor/dist/cjs/spl/token';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import { Cluster, Connection, Keypair } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { I80F48, ZERO_I80F48 } from '../accounts/I80F48';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
import { toUiDecimals } 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 GROUP_NUM = Number(process.env.GROUP_NUM || 2);
|
||||
const CLUSTER: Cluster =
|
||||
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
|
||||
|
||||
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'))),
|
||||
);
|
||||
|
||||
const adminWallet = new Wallet(admin);
|
||||
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);
|
||||
console.log(`Group ${group.publicKey.toBase58()}`);
|
||||
console.log(`${group.toString()}`);
|
||||
|
||||
const banks = Array.from(group.banksMapByMint.values()).flat();
|
||||
const banksMapUsingTokenIndex = new Map(
|
||||
banks.map((bank) => {
|
||||
(bank as any).indexedDepositsByMangoAccounts = ZERO_I80F48;
|
||||
(bank as any).indexedBorrowsByMangoAccounts = ZERO_I80F48;
|
||||
(bank as any).indexedDepositsByMangoAccounts = ZERO_I80F48();
|
||||
(bank as any).indexedBorrowsByMangoAccounts = ZERO_I80F48();
|
||||
return [bank.tokenIndex, bank];
|
||||
}),
|
||||
);
|
||||
|
@ -51,7 +60,7 @@ async function main() {
|
|||
bank as any
|
||||
).indexedDepositsByMangoAccounts.add(
|
||||
token.indexedPosition.mul(
|
||||
banksMapUsingTokenIndex.get(token.tokenIndex).depositIndex,
|
||||
banksMapUsingTokenIndex.get(token.tokenIndex)!.depositIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -61,7 +70,7 @@ async function main() {
|
|||
).indexedBorrowsByMangoAccounts.add(
|
||||
token.indexedPosition
|
||||
.abs()
|
||||
.mul(banksMapUsingTokenIndex.get(token.tokenIndex).borrowIndex),
|
||||
.mul(banksMapUsingTokenIndex.get(token.tokenIndex)!.borrowIndex),
|
||||
);
|
||||
}
|
||||
}),
|
||||
|
@ -74,7 +83,7 @@ async function main() {
|
|||
coder()
|
||||
.accounts.decode(
|
||||
'token',
|
||||
(await client.program.provider.connection.getAccountInfo(bank.vault))
|
||||
(await client.program.provider.connection.getAccountInfo(bank.vault))!
|
||||
.data,
|
||||
)
|
||||
.amount.toNumber(),
|
||||
|
@ -94,7 +103,7 @@ async function main() {
|
|||
`\n ${'bank'.padEnd(40)} ${bank.publicKey}` +
|
||||
`\n ${'vault'.padEnd(40)} ${bank.vault}` +
|
||||
`\n ${'mint'.padEnd(40)} ${bank.mint}` +
|
||||
`\n ${'price'.padEnd(40)} ${bank.price.toNumber()}` +
|
||||
`\n ${'price'.padEnd(40)} ${bank.price?.toNumber()}` +
|
||||
`\n ${'uiPrice'.padEnd(40)} ${bank.uiPrice}` +
|
||||
`\n ${'error'.padEnd(40)} ${error}` +
|
||||
`\n ${'collectedFeesNative'.padEnd(40)} ${bank.collectedFeesNative}` +
|
|
@ -20,24 +20,29 @@ async function debugUser(
|
|||
console.log(
|
||||
'buildFixedAccountRetrieverHealthAccounts ' +
|
||||
client
|
||||
.buildFixedAccountRetrieverHealthAccounts(group, mangoAccount, [
|
||||
group.banksMapByName.get('BTC')[0],
|
||||
group.banksMapByName.get('USDC')[0],
|
||||
])
|
||||
.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().toNumber()),
|
||||
toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getHealth(HealthType.init) ' +
|
||||
toUiDecimalsForQuote(mangoAccount.getHealth(HealthType.init).toNumber()),
|
||||
toUiDecimalsForQuote(mangoAccount.getHealth(HealthType.init)!.toNumber()),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getHealthRatio(HealthType.init) ' +
|
||||
mangoAccount.getHealthRatio(HealthType.init).toNumber(),
|
||||
mangoAccount.getHealthRatio(HealthType.init)!.toNumber(),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getHealthRatioUi(HealthType.init) ' +
|
||||
|
@ -45,7 +50,7 @@ async function debugUser(
|
|||
);
|
||||
console.log(
|
||||
'mangoAccount.getHealthRatio(HealthType.maint) ' +
|
||||
mangoAccount.getHealthRatio(HealthType.maint).toNumber(),
|
||||
mangoAccount.getHealthRatio(HealthType.maint)!.toNumber(),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getHealthRatioUi(HealthType.maint) ' +
|
||||
|
@ -53,18 +58,18 @@ async function debugUser(
|
|||
);
|
||||
console.log(
|
||||
'mangoAccount.getCollateralValue() ' +
|
||||
toUiDecimalsForQuote(mangoAccount.getCollateralValue().toNumber()),
|
||||
toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getAssetsValue() ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getAssetsValue(HealthType.init).toNumber(),
|
||||
mangoAccount.getAssetsValue(HealthType.init)!.toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getLiabsValue() ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getLiabsValue(HealthType.init).toNumber(),
|
||||
mangoAccount.getLiabsValue(HealthType.init)!.toNumber(),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -73,7 +78,7 @@ async function debugUser(
|
|||
`mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, ${token}) ` +
|
||||
mangoAccount.getMaxWithdrawWithBorrowForTokenUi(
|
||||
group,
|
||||
group.banksMapByName.get(token)[0].mint,
|
||||
group.banksMapByName.get(token)![0].mint,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -89,29 +94,28 @@ async function debugUser(
|
|||
}
|
||||
for (const srcToken of Array.from(group.banksMapByName.keys())) {
|
||||
simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} 1 `, {
|
||||
mintPk: group.banksMapByName.get(srcToken)[0].mint,
|
||||
mintPk: group.banksMapByName.get(srcToken)![0].mint,
|
||||
uiTokenAmount: 1,
|
||||
});
|
||||
simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} -1 `, {
|
||||
mintPk: group.banksMapByName.get(srcToken)[0].mint,
|
||||
mintPk: group.banksMapByName.get(srcToken)![0].mint,
|
||||
uiTokenAmount: -1,
|
||||
});
|
||||
}
|
||||
|
||||
function getMaxSourceForTokenSwapWrapper(src, tgt) {
|
||||
// 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,
|
||||
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),
|
||||
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
|
||||
),
|
||||
)
|
||||
.toNumber(),
|
||||
|
@ -148,8 +152,8 @@ async function main() {
|
|||
const group = await client.getGroupForCreator(admin.publicKey, 2);
|
||||
|
||||
for (const keypair of [
|
||||
process.env.MB_PAYER_KEYPAIR,
|
||||
process.env.MB_USER2_KEYPAIR,
|
||||
process.env.MB_PAYER_KEYPAIR!,
|
||||
process.env.MB_USER2_KEYPAIR!,
|
||||
]) {
|
||||
console.log();
|
||||
const user = Keypair.fromSecretKey(
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
||||
|
||||
// Reference
|
||||
// https://explorer.solana.com/
|
||||
// https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['USDT', 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], // Wrapped Bitcoin (Sollet)
|
||||
['ETH', '7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs'], // Ether (Portal), will be treat as ETH due to higher liquidity
|
||||
['soETH', '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk'], // Wrapped Ethereum (Sollet), will be treated as soETH
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
['mSOL', 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So'],
|
||||
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
|
||||
]);
|
||||
|
||||
// Reference
|
||||
// https://pyth.network/price-feeds/
|
||||
// https://switchboard.xyz/explorer
|
||||
const MAINNET_ORACLES = new Map([
|
||||
['USDT', '3vxLXJqLqF3JG5TCbYycbKWRBbCJQLxQmBGCkyqEEefL'],
|
||||
['BTC', 'GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU'],
|
||||
['ETH', 'JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB'],
|
||||
['soETH', 'JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB'],
|
||||
['SOL', 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG'],
|
||||
['mSOL', 'E4v1BBgoso9s64TQvmyownAVJbhbEPGyzA3qn4n46qj9'],
|
||||
['MNGO', '79wm3jjcPr6RaNQ4DGvP5KxG1mNd3gEBsg6FsNVFezK4'],
|
||||
]);
|
||||
|
||||
async function createGroup() {
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.MB_CLUSTER_URL!, options);
|
||||
|
||||
const adminWallet = new Wallet(admin);
|
||||
console.log(`Admin ${adminWallet.publicKey.toBase58()}`);
|
||||
const adminProvider = new AnchorProvider(connection, adminWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
adminProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
{},
|
||||
'get-program-accounts',
|
||||
);
|
||||
|
||||
console.log(`Creating Group...`);
|
||||
const insuranceMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
||||
await client.groupCreate(
|
||||
GROUP_NUM,
|
||||
true /* with intention */,
|
||||
0 /* since spot and perp features are not finished */,
|
||||
insuranceMint,
|
||||
);
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(`...registered group ${group.publicKey}`);
|
||||
}
|
||||
|
||||
async function registerTokens() {
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.MB_CLUSTER_URL!, options);
|
||||
|
||||
const adminWallet = new Wallet(admin);
|
||||
console.log(`Admin ${adminWallet.publicKey.toBase58()}`);
|
||||
const adminProvider = new AnchorProvider(connection, adminWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
adminProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
{},
|
||||
'get-program-accounts' /* idsjson service doesn't know about this group yet */,
|
||||
);
|
||||
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
|
||||
console.log(`Creating USDC stub oracle...`);
|
||||
const usdcMainnetMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
||||
await client.stubOracleCreate(group, usdcMainnetMint, 1.0);
|
||||
const usdcMainnetOracle = (
|
||||
await client.getStubOracle(group, usdcMainnetMint)
|
||||
)[0];
|
||||
console.log(`...created stub oracle ${usdcMainnetOracle.publicKey}`);
|
||||
|
||||
console.log(`Registering USDC...`);
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
usdcMainnetMint,
|
||||
usdcMainnetOracle.publicKey,
|
||||
0.1,
|
||||
0, // insurance vault token should be the first to be registered
|
||||
'USDC',
|
||||
0.004, // rate parameters are chosen to be the same for all high asset weight tokens,
|
||||
// hoping that dynamic rate parameter adjustment would be enough to tune their rates to the markets needs
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005, // 50 bps
|
||||
0.0005, // 5 bps
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
);
|
||||
|
||||
console.log(`Registering USDT...`);
|
||||
const usdtMainnetMint = new PublicKey(MAINNET_MINTS.get('USDT')!);
|
||||
const usdtMainnetOracle = new PublicKey(MAINNET_ORACLES.get('USDT')!);
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
usdtMainnetMint,
|
||||
usdtMainnetOracle,
|
||||
0.1,
|
||||
1,
|
||||
'USDT',
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.95,
|
||||
0.9,
|
||||
1.05,
|
||||
1.1,
|
||||
0.025, // rule of thumb used - half of maintLiabWeight
|
||||
);
|
||||
|
||||
console.log(`Registering BTC...`);
|
||||
const btcMainnetMint = new PublicKey(MAINNET_MINTS.get('BTC')!);
|
||||
const btcMainnetOracle = new PublicKey(MAINNET_ORACLES.get('BTC')!);
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
btcMainnetMint,
|
||||
btcMainnetOracle,
|
||||
0.1,
|
||||
2,
|
||||
'BTC',
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
|
||||
console.log(`Registering ETH...`);
|
||||
const ethMainnetMint = new PublicKey(MAINNET_MINTS.get('ETH')!);
|
||||
const ethMainnetOracle = new PublicKey(MAINNET_ORACLES.get('ETH')!);
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
ethMainnetMint,
|
||||
ethMainnetOracle,
|
||||
0.1,
|
||||
3,
|
||||
'ETH',
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
|
||||
console.log(`Registering soETH...`);
|
||||
const soEthMainnetMint = new PublicKey(MAINNET_MINTS.get('soETH')!);
|
||||
const soEthMainnetOracle = new PublicKey(MAINNET_ORACLES.get('soETH')!);
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
soEthMainnetMint,
|
||||
soEthMainnetOracle,
|
||||
0.1,
|
||||
4,
|
||||
'soETH',
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
|
||||
console.log(`Registering SOL...`);
|
||||
const solMainnetMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
|
||||
const solMainnetOracle = new PublicKey(MAINNET_ORACLES.get('SOL')!);
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
solMainnetMint,
|
||||
solMainnetOracle,
|
||||
0.1,
|
||||
5,
|
||||
'SOL',
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
|
||||
console.log(`Registering mSOL...`);
|
||||
const msolMainnetMint = new PublicKey(MAINNET_MINTS.get('mSOL')!);
|
||||
const msolMainnetOracle = new PublicKey(MAINNET_ORACLES.get('mSOL')!);
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
msolMainnetMint,
|
||||
msolMainnetOracle,
|
||||
0.1,
|
||||
6,
|
||||
'mSOL',
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
|
||||
// log tokens/banks
|
||||
await group.reloadAll(client);
|
||||
for (const [bank] of await group.banksMapByMint.values()) {
|
||||
console.log(`${bank.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
createGroup();
|
||||
registerTokens();
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
|
@ -730,12 +730,7 @@ export type MangoV4 = {
|
|||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "tokenIndex",
|
||||
"type": "u16"
|
||||
}
|
||||
]
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "tokenUpdateIndexAndRate",
|
||||
|
@ -1106,6 +1101,11 @@ export type MangoV4 = {
|
|||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "tokenAccount",
|
||||
"isMut": true,
|
||||
|
@ -1157,6 +1157,11 @@ export type MangoV4 = {
|
|||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "tokenAccount",
|
||||
"isMut": true,
|
||||
|
@ -1233,6 +1238,36 @@ export type MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "healthRegionBegin",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "instructions",
|
||||
"isMut": false,
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"Instructions Sysvar for instruction introspection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "healthRegionEnd",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "serum3RegisterMarket",
|
||||
"docs": [
|
||||
|
@ -1285,6 +1320,30 @@ export type MangoV4 = {
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "indexReservation",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"type": "string",
|
||||
"value": "Serum3Index"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"type": "publicKey",
|
||||
"path": "group"
|
||||
},
|
||||
{
|
||||
"kind": "arg",
|
||||
"type": "u16",
|
||||
"path": "market_index"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "quoteBank",
|
||||
"isMut": false,
|
||||
|
@ -1546,24 +1605,20 @@ export type MangoV4 = {
|
|||
]
|
||||
},
|
||||
{
|
||||
"name": "quoteBank",
|
||||
"name": "payerBank",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"The bank that pays for the order, if necessary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "quoteVault",
|
||||
"name": "payerVault",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "baseBank",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "baseVault",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"The bank vault that pays for the order, if necessary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
|
@ -3000,12 +3055,21 @@ export type MangoV4 = {
|
|||
"",
|
||||
"Normally accounts can not be liquidated while maint_health >= 0. But when an account",
|
||||
"reaches maint_health < 0, liquidators will call a liquidation instruction and thereby",
|
||||
"set this flag. Now the account may be liquidated until init_health >= 0."
|
||||
"set this flag. Now the account may be liquidated until init_health >= 0.",
|
||||
"",
|
||||
"Many actions should be disabled while the account is being liquidated, even if",
|
||||
"its maint health has recovered to positive. Creating new open orders would, for example,",
|
||||
"confuse liquidators."
|
||||
],
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding2",
|
||||
"name": "inHealthRegion",
|
||||
"docs": [
|
||||
"The account is currently inside a health region marked by HealthRegionBegin...HealthRegionEnd.",
|
||||
"",
|
||||
"Must never be set after a transaction ends."
|
||||
],
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
|
@ -3029,12 +3093,19 @@ export type MangoV4 = {
|
|||
"name": "netSettled",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "healthRegionPreInitHealth",
|
||||
"docs": [
|
||||
"Init health as calculated during HealthReginBegin, rounded up."
|
||||
],
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
248
|
||||
240
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -3095,7 +3166,7 @@ export type MangoV4 = {
|
|||
"name": "perpOpenOrders",
|
||||
"type": {
|
||||
"vec": {
|
||||
"defined": "PerpOpenOrders"
|
||||
"defined": "PerpOpenOrder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3789,6 +3860,10 @@ export type MangoV4 = {
|
|||
{
|
||||
"name": "quoteIndex",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "marketIndex",
|
||||
"type": "u16"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -3798,6 +3873,10 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "perpMarketIndex",
|
||||
"type": "u16"
|
||||
},
|
||||
{
|
||||
"name": "maintAssetWeight",
|
||||
"type": {
|
||||
|
@ -3932,11 +4011,17 @@ export type MangoV4 = {
|
|||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "previousNativeCoinReserved",
|
||||
"name": "baseBorrowsWithoutFee",
|
||||
"docs": [
|
||||
"Tracks the amount of borrows that have flowed into the serum open orders account.",
|
||||
"These borrows did not have the loan origination fee applied, and that may happen",
|
||||
"later (in serum3_settle_funds) if we can guarantee that the funds were used.",
|
||||
"In particular a place-on-book, cancel, settle should not cost fees."
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "previousNativePcReserved",
|
||||
"name": "quoteBorrowsWithoutFee",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
|
@ -4077,7 +4162,7 @@ export type MangoV4 = {
|
|||
}
|
||||
},
|
||||
{
|
||||
"name": "PerpOpenOrders",
|
||||
"name": "PerpOpenOrder",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
|
@ -5146,43 +5231,53 @@ export type MangoV4 = {
|
|||
},
|
||||
{
|
||||
"code": 6007,
|
||||
"name": "HealthMustBePositiveOrIncrease",
|
||||
"msg": "health must be positive or increase"
|
||||
},
|
||||
{
|
||||
"code": 6008,
|
||||
"name": "HealthMustBeNegative",
|
||||
"msg": "health must be negative"
|
||||
},
|
||||
{
|
||||
"code": 6008,
|
||||
"code": 6009,
|
||||
"name": "IsBankrupt",
|
||||
"msg": "the account is bankrupt"
|
||||
},
|
||||
{
|
||||
"code": 6009,
|
||||
"code": 6010,
|
||||
"name": "IsNotBankrupt",
|
||||
"msg": "the account is not bankrupt"
|
||||
},
|
||||
{
|
||||
"code": 6010,
|
||||
"code": 6011,
|
||||
"name": "NoFreeTokenPositionIndex",
|
||||
"msg": "no free token position index"
|
||||
},
|
||||
{
|
||||
"code": 6011,
|
||||
"code": 6012,
|
||||
"name": "NoFreeSerum3OpenOrdersIndex",
|
||||
"msg": "no free serum3 open orders index"
|
||||
},
|
||||
{
|
||||
"code": 6012,
|
||||
"code": 6013,
|
||||
"name": "NoFreePerpPositionIndex",
|
||||
"msg": "no free perp position index"
|
||||
},
|
||||
{
|
||||
"code": 6013,
|
||||
"code": 6014,
|
||||
"name": "Serum3OpenOrdersExistAlready",
|
||||
"msg": "serum3 open orders exist already"
|
||||
},
|
||||
{
|
||||
"code": 6014,
|
||||
"code": 6015,
|
||||
"name": "InsufficentBankVaultFunds",
|
||||
"msg": "bank vault has insufficent funds"
|
||||
},
|
||||
{
|
||||
"code": 6016,
|
||||
"name": "BeingLiquidated",
|
||||
"msg": "account is currently being liquidated"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -5919,12 +6014,7 @@ export const IDL: MangoV4 = {
|
|||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "tokenIndex",
|
||||
"type": "u16"
|
||||
}
|
||||
]
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "tokenUpdateIndexAndRate",
|
||||
|
@ -6295,6 +6385,11 @@ export const IDL: MangoV4 = {
|
|||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "tokenAccount",
|
||||
"isMut": true,
|
||||
|
@ -6346,6 +6441,11 @@ export const IDL: MangoV4 = {
|
|||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "tokenAccount",
|
||||
"isMut": true,
|
||||
|
@ -6422,6 +6522,36 @@ export const IDL: MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "healthRegionBegin",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "instructions",
|
||||
"isMut": false,
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"Instructions Sysvar for instruction introspection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "healthRegionEnd",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "serum3RegisterMarket",
|
||||
"docs": [
|
||||
|
@ -6474,6 +6604,30 @@ export const IDL: MangoV4 = {
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "indexReservation",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"type": "string",
|
||||
"value": "Serum3Index"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"type": "publicKey",
|
||||
"path": "group"
|
||||
},
|
||||
{
|
||||
"kind": "arg",
|
||||
"type": "u16",
|
||||
"path": "market_index"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "quoteBank",
|
||||
"isMut": false,
|
||||
|
@ -6735,24 +6889,20 @@ export const IDL: MangoV4 = {
|
|||
]
|
||||
},
|
||||
{
|
||||
"name": "quoteBank",
|
||||
"name": "payerBank",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"The bank that pays for the order, if necessary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "quoteVault",
|
||||
"name": "payerVault",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "baseBank",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "baseVault",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"The bank vault that pays for the order, if necessary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
|
@ -8189,12 +8339,21 @@ export const IDL: MangoV4 = {
|
|||
"",
|
||||
"Normally accounts can not be liquidated while maint_health >= 0. But when an account",
|
||||
"reaches maint_health < 0, liquidators will call a liquidation instruction and thereby",
|
||||
"set this flag. Now the account may be liquidated until init_health >= 0."
|
||||
"set this flag. Now the account may be liquidated until init_health >= 0.",
|
||||
"",
|
||||
"Many actions should be disabled while the account is being liquidated, even if",
|
||||
"its maint health has recovered to positive. Creating new open orders would, for example,",
|
||||
"confuse liquidators."
|
||||
],
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding2",
|
||||
"name": "inHealthRegion",
|
||||
"docs": [
|
||||
"The account is currently inside a health region marked by HealthRegionBegin...HealthRegionEnd.",
|
||||
"",
|
||||
"Must never be set after a transaction ends."
|
||||
],
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
|
@ -8218,12 +8377,19 @@ export const IDL: MangoV4 = {
|
|||
"name": "netSettled",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "healthRegionPreInitHealth",
|
||||
"docs": [
|
||||
"Init health as calculated during HealthReginBegin, rounded up."
|
||||
],
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
248
|
||||
240
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -8284,7 +8450,7 @@ export const IDL: MangoV4 = {
|
|||
"name": "perpOpenOrders",
|
||||
"type": {
|
||||
"vec": {
|
||||
"defined": "PerpOpenOrders"
|
||||
"defined": "PerpOpenOrder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8978,6 +9144,10 @@ export const IDL: MangoV4 = {
|
|||
{
|
||||
"name": "quoteIndex",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "marketIndex",
|
||||
"type": "u16"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -8987,6 +9157,10 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "perpMarketIndex",
|
||||
"type": "u16"
|
||||
},
|
||||
{
|
||||
"name": "maintAssetWeight",
|
||||
"type": {
|
||||
|
@ -9121,11 +9295,17 @@ export const IDL: MangoV4 = {
|
|||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "previousNativeCoinReserved",
|
||||
"name": "baseBorrowsWithoutFee",
|
||||
"docs": [
|
||||
"Tracks the amount of borrows that have flowed into the serum open orders account.",
|
||||
"These borrows did not have the loan origination fee applied, and that may happen",
|
||||
"later (in serum3_settle_funds) if we can guarantee that the funds were used.",
|
||||
"In particular a place-on-book, cancel, settle should not cost fees."
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "previousNativePcReserved",
|
||||
"name": "quoteBorrowsWithoutFee",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
|
@ -9266,7 +9446,7 @@ export const IDL: MangoV4 = {
|
|||
}
|
||||
},
|
||||
{
|
||||
"name": "PerpOpenOrders",
|
||||
"name": "PerpOpenOrder",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
|
@ -10335,43 +10515,53 @@ export const IDL: MangoV4 = {
|
|||
},
|
||||
{
|
||||
"code": 6007,
|
||||
"name": "HealthMustBePositiveOrIncrease",
|
||||
"msg": "health must be positive or increase"
|
||||
},
|
||||
{
|
||||
"code": 6008,
|
||||
"name": "HealthMustBeNegative",
|
||||
"msg": "health must be negative"
|
||||
},
|
||||
{
|
||||
"code": 6008,
|
||||
"code": 6009,
|
||||
"name": "IsBankrupt",
|
||||
"msg": "the account is bankrupt"
|
||||
},
|
||||
{
|
||||
"code": 6009,
|
||||
"code": 6010,
|
||||
"name": "IsNotBankrupt",
|
||||
"msg": "the account is not bankrupt"
|
||||
},
|
||||
{
|
||||
"code": 6010,
|
||||
"code": 6011,
|
||||
"name": "NoFreeTokenPositionIndex",
|
||||
"msg": "no free token position index"
|
||||
},
|
||||
{
|
||||
"code": 6011,
|
||||
"code": 6012,
|
||||
"name": "NoFreeSerum3OpenOrdersIndex",
|
||||
"msg": "no free serum3 open orders index"
|
||||
},
|
||||
{
|
||||
"code": 6012,
|
||||
"code": 6013,
|
||||
"name": "NoFreePerpPositionIndex",
|
||||
"msg": "no free perp position index"
|
||||
},
|
||||
{
|
||||
"code": 6013,
|
||||
"code": 6014,
|
||||
"name": "Serum3OpenOrdersExistAlready",
|
||||
"msg": "serum3 open orders exist already"
|
||||
},
|
||||
{
|
||||
"code": 6014,
|
||||
"code": 6015,
|
||||
"name": "InsufficentBankVaultFunds",
|
||||
"msg": "bank vault has insufficent funds"
|
||||
},
|
||||
{
|
||||
"code": 6016,
|
||||
"name": "BeingLiquidated",
|
||||
"msg": "account is currently being liquidated"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
@ -17,6 +17,8 @@ import { MANGO_V4_ID } from '../constants';
|
|||
const DEVNET_SERUM3_MARKETS = new Map([
|
||||
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
|
||||
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
|
||||
['ETH/USDC', 'BkAraCyL9TTLbeMY3L1VWrPcv32DvSi5QDDQjik1J6Ac'],
|
||||
['SRM/USDC', '249LDNPLLL29nRq8kjBTg9hKdXMcZf4vK2UvxszZYcuZ'],
|
||||
]);
|
||||
const DEVNET_MINTS = new Map([
|
||||
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
|
||||
|
@ -24,12 +26,16 @@ const DEVNET_MINTS = new Map([
|
|||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
|
||||
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
|
||||
['ETH', 'Cu84KB3tDL6SbFgToHMLYVDJJXdJjenNzSKikeAvzmkA'],
|
||||
['SRM', 'AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH'],
|
||||
]);
|
||||
const DEVNET_ORACLES = new Map([
|
||||
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
|
||||
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
|
||||
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
|
||||
['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'],
|
||||
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
|
||||
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
|
||||
]);
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
||||
|
@ -88,19 +94,19 @@ async function main() {
|
|||
0.1,
|
||||
0, // tokenIndex
|
||||
'USDC',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.9,
|
||||
1.5,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.0005,
|
||||
0.8,
|
||||
0.6,
|
||||
1.2,
|
||||
1.4,
|
||||
0.02,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {}
|
||||
|
@ -117,19 +123,19 @@ async function main() {
|
|||
0.1,
|
||||
1, // tokenIndex
|
||||
'BTC',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.88,
|
||||
0.0005,
|
||||
0.0005,
|
||||
0.8,
|
||||
0.6,
|
||||
1.1,
|
||||
1.2,
|
||||
1.4,
|
||||
0.02,
|
||||
0.05,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
|
@ -148,19 +154,19 @@ async function main() {
|
|||
0.1,
|
||||
2, // tokenIndex
|
||||
'SOL',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.63,
|
||||
0.0005,
|
||||
0.0005,
|
||||
0.8,
|
||||
0.6,
|
||||
1.1,
|
||||
1.2,
|
||||
1.4,
|
||||
0.02,
|
||||
0.05,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
|
@ -198,14 +204,75 @@ async function main() {
|
|||
console.log(error);
|
||||
}
|
||||
|
||||
// register token 4
|
||||
// register token 7
|
||||
console.log(`Registering ETH...`);
|
||||
const ethDevnetMint = new PublicKey(DEVNET_MINTS.get('ETH')!);
|
||||
const ethDevnetOracle = new PublicKey(DEVNET_ORACLES.get('ETH')!);
|
||||
try {
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
ethDevnetMint,
|
||||
ethDevnetOracle,
|
||||
0.1,
|
||||
7, // tokenIndex
|
||||
'ETH',
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// register token 5
|
||||
console.log(`Registering SRM...`);
|
||||
const srmDevnetMint = new PublicKey(DEVNET_MINTS.get('SRM')!);
|
||||
const srmDevnetOracle = new PublicKey(DEVNET_ORACLES.get('SRM')!);
|
||||
try {
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
srmDevnetMint,
|
||||
srmDevnetOracle,
|
||||
0.1,
|
||||
5, // tokenIndex
|
||||
'SRM',
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Editing group, setting existing admin as fastListingAdmin to be able to add MNGO truslessly...`,
|
||||
);
|
||||
let sig = await client.groupEdit(
|
||||
group,
|
||||
group.admin,
|
||||
new PublicKey('Efhak3qj3MiyzgJr3cUUqXXz5wr3oYHt9sPzuqJf9eBN'),
|
||||
group.admin,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
@ -231,7 +298,7 @@ async function main() {
|
|||
|
||||
// register serum market
|
||||
console.log(`Registering serum3 market...`);
|
||||
const serumMarketExternalPk = new PublicKey(
|
||||
let serumMarketExternalPk = new PublicKey(
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
try {
|
||||
|
@ -251,7 +318,35 @@ async function main() {
|
|||
group.getFirstBankByMint(btcDevnetMint).tokenIndex,
|
||||
group.getFirstBankByMint(usdcDevnetMint).tokenIndex,
|
||||
);
|
||||
console.log(`...registerd serum3 market ${markets[0].publicKey}`);
|
||||
console.log(`...registered serum3 market ${markets[0].publicKey}`);
|
||||
|
||||
serumMarketExternalPk = new PublicKey(DEVNET_SERUM3_MARKETS.get('ETH/USDC')!);
|
||||
try {
|
||||
await client.serum3RegisterMarket(
|
||||
group,
|
||||
serumMarketExternalPk,
|
||||
group.getFirstBankByMint(ethDevnetMint),
|
||||
group.getFirstBankByMint(usdcDevnetMint),
|
||||
0,
|
||||
'ETH/USDC',
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
serumMarketExternalPk = new PublicKey(DEVNET_SERUM3_MARKETS.get('SRM/USDC')!);
|
||||
try {
|
||||
await client.serum3RegisterMarket(
|
||||
group,
|
||||
serumMarketExternalPk,
|
||||
group.getFirstBankByMint(srmDevnetMint),
|
||||
group.getFirstBankByMint(usdcDevnetMint),
|
||||
0,
|
||||
'SRM/USDC',
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// register perp market
|
||||
console.log(`Registering perp market...`);
|
||||
|
@ -292,114 +387,93 @@ async function main() {
|
|||
// edit
|
||||
//
|
||||
|
||||
console.log(`Editing USDC...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
usdcDevnetMint,
|
||||
btcDevnetOracle,
|
||||
0.1,
|
||||
undefined,
|
||||
0.01,
|
||||
0.3,
|
||||
0.08,
|
||||
0.81,
|
||||
0.91,
|
||||
0.75,
|
||||
0.0007,
|
||||
1.7,
|
||||
0.9,
|
||||
0.7,
|
||||
1.3,
|
||||
1.5,
|
||||
0.04,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
console.log(`Resetting USDC...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
0.1,
|
||||
undefined,
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.9,
|
||||
1.5,
|
||||
0.0005,
|
||||
0.0005,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
0.02,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.getFirstBankByMint(usdcDevnetMint).toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
if (true) {
|
||||
console.log(`Editing USDC...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
0.1,
|
||||
undefined,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(`Editing perp market...`);
|
||||
try {
|
||||
let sig = await client.perpEditMarket(
|
||||
group,
|
||||
'BTC-PERP',
|
||||
btcDevnetOracle,
|
||||
0.2,
|
||||
0,
|
||||
6,
|
||||
0.9,
|
||||
0.9,
|
||||
1.035,
|
||||
1.06,
|
||||
0.013,
|
||||
0.0003,
|
||||
0.1,
|
||||
0.07,
|
||||
0.07,
|
||||
1001,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.perpMarketsMap.get('BTC-PERP')!.toString());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
console.log(`Resetting perp market...`);
|
||||
try {
|
||||
let sig = await client.perpEditMarket(
|
||||
group,
|
||||
'BTC-PERP',
|
||||
btcDevnetOracle,
|
||||
0.1,
|
||||
1,
|
||||
6,
|
||||
1,
|
||||
0.95,
|
||||
1.025,
|
||||
1.05,
|
||||
0.012,
|
||||
0.0002,
|
||||
0.0,
|
||||
0.05,
|
||||
0.05,
|
||||
100,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.perpMarketsMap.get('BTC-PERP')!.toString());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log(`Editing BTC...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
0.1,
|
||||
undefined,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(`Editing SOL...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
0.1,
|
||||
undefined,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
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();
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { Serum3Side } from '../accounts/serum3';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// script which shows how to close a mango account cleanly i.e. close all active positions, withdraw all tokens, etc.
|
||||
//
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
||||
|
||||
// note: either use finalized or expect closing certain things to fail and having to runs scrript multiple times
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
|
||||
// note: see note above
|
||||
// options.commitment = 'finalized';
|
||||
|
||||
const connection = new Connection(
|
||||
'https://mango.devnet.rpcpool.com',
|
||||
options,
|
||||
);
|
||||
|
||||
// user
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.USER2_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'devnet',
|
||||
MANGO_V4_ID['devnet'],
|
||||
{},
|
||||
'get-program-accounts',
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
try {
|
||||
// fetch group
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(`Found group ${group.publicKey.toBase58()}`);
|
||||
|
||||
// fetch account
|
||||
const mangoAccount = (
|
||||
await client.getMangoAccountsForOwner(group, user.publicKey)
|
||||
)[0];
|
||||
console.log(`...found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
// close mango account serum3 positions, closing might require cancelling orders and settling
|
||||
for (const serum3Account of mangoAccount.serum3Active()) {
|
||||
let orders = await client.getSerum3Orders(
|
||||
group,
|
||||
group.findSerum3Market(serum3Account.marketIndex)!.name,
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(
|
||||
` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
|
||||
);
|
||||
console.log(` - Cancelling order with ${order.orderId}`);
|
||||
await client.serum3CancelOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
|
||||
order.orderId,
|
||||
);
|
||||
}
|
||||
await client.serum3SettleFunds(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.findSerum3Market(serum3Account.marketIndex)!.name,
|
||||
);
|
||||
await client.serum3CloseOpenOrders(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.findSerum3Market(serum3Account.marketIndex)!.name,
|
||||
);
|
||||
}
|
||||
|
||||
// we closed a serum account, this changes the health accounts we are passing in for future ixs
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
// withdraw all tokens
|
||||
for (const token of mangoAccount.tokensActive()) {
|
||||
let native = token.balance(
|
||||
group.getFirstBankByTokenIndex(token.tokenIndex),
|
||||
);
|
||||
|
||||
// to avoid rounding issues
|
||||
if (native.toNumber() < 1) {
|
||||
continue;
|
||||
}
|
||||
let nativeFlooredNumber = Math.floor(native.toNumber());
|
||||
console.log(
|
||||
`withdrawing token ${
|
||||
group.getFirstBankByTokenIndex(token.tokenIndex).name
|
||||
} native amount ${nativeFlooredNumber} `,
|
||||
);
|
||||
|
||||
await client.tokenWithdrawNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.getFirstBankByTokenIndex(token.tokenIndex).mint,
|
||||
nativeFlooredNumber - 1 /* see comment in token_withdraw in program */,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// reload and print current positions
|
||||
await mangoAccount.reload(client, group);
|
||||
console.log(`...mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
// close account
|
||||
console.log(`Close mango account...`);
|
||||
const res = await client.closeMangoAccount(group, mangoAccount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,134 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// An example for users based on high level api i.e. the client
|
||||
// Create
|
||||
// process.env.USER_KEYPAIR - mango account owner keypair path
|
||||
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
|
||||
//
|
||||
// This script deposits some tokens, places some serum orders, cancels them, places some perp orders
|
||||
//
|
||||
|
||||
const DEVNET_MINTS = new Map([
|
||||
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
|
||||
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
|
||||
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
|
||||
['ETH', 'Cu84KB3tDL6SbFgToHMLYVDJJXdJjenNzSKikeAvzmkA'],
|
||||
['SRM', 'AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH'],
|
||||
]);
|
||||
const DEVNET_ORACLES = new Map([
|
||||
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
|
||||
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
|
||||
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
|
||||
['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'],
|
||||
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
|
||||
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
|
||||
]);
|
||||
export const DEVNET_SERUM3_MARKETS = new Map([
|
||||
['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')],
|
||||
['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')],
|
||||
]);
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(
|
||||
'https://mango.devnet.rpcpool.com',
|
||||
options,
|
||||
);
|
||||
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.USER2_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'devnet',
|
||||
MANGO_V4_ID['devnet'],
|
||||
{},
|
||||
'get-program-accounts',
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
// fetch group
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(`${group}`);
|
||||
|
||||
// create + fetch account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
const mangoAccount = await client.getOrCreateMangoAccount(
|
||||
group,
|
||||
user.publicKey,
|
||||
);
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString(group));
|
||||
|
||||
if (true) {
|
||||
// deposit and withdraw
|
||||
|
||||
try {
|
||||
console.log(`...depositing`);
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('USDC')!),
|
||||
1000,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('MNGO')!),
|
||||
100,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('ETH')!),
|
||||
500,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('SRM')!),
|
||||
500,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('BTC')!),
|
||||
1,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(mangoAccount.toString(group));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -94,7 +94,7 @@ async function main() {
|
|||
|
||||
// withdraw all tokens
|
||||
for (const token of mangoAccount.tokensActive()) {
|
||||
let native = token.native(
|
||||
let native = token.balance(
|
||||
group.getFirstBankByTokenIndex(token.tokenIndex),
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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 { OrderType, Side } from '../accounts/perp';
|
||||
import {
|
||||
|
@ -28,6 +29,10 @@ const DEVNET_MINTS = new Map([
|
|||
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
|
||||
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
|
||||
]);
|
||||
export const DEVNET_SERUM3_MARKETS = new Map([
|
||||
['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')],
|
||||
['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')],
|
||||
]);
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
||||
|
||||
|
@ -61,7 +66,7 @@ async function main() {
|
|||
),
|
||||
);
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(group.toString());
|
||||
console.log(`${group}`);
|
||||
|
||||
// create + fetch account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
|
@ -70,9 +75,11 @@ async function main() {
|
|||
user.publicKey,
|
||||
);
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
console.log(mangoAccount.toString(group));
|
||||
|
||||
if (true) {
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
if (false) {
|
||||
// set delegate, and change name
|
||||
console.log(`...changing mango account name, and setting a delegate`);
|
||||
const randomKey = new PublicKey(
|
||||
|
@ -99,7 +106,8 @@ async function main() {
|
|||
console.log(mangoAccount.toString());
|
||||
}
|
||||
|
||||
if (true) {
|
||||
if (false) {
|
||||
// expand account
|
||||
console.log(
|
||||
`...expanding mango account to have serum3 and perp position slots`,
|
||||
);
|
||||
|
@ -107,11 +115,11 @@ async function main() {
|
|||
await mangoAccount.reload(client, group);
|
||||
}
|
||||
|
||||
if (true) {
|
||||
if (false) {
|
||||
// deposit and withdraw
|
||||
|
||||
try {
|
||||
console.log(`...depositing 50 USDC`);
|
||||
console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`);
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
@ -120,6 +128,22 @@ async function main() {
|
|||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('SOL')!),
|
||||
1,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('MNGO')!),
|
||||
1,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...withdrawing 1 USDC`);
|
||||
await client.tokenWithdraw(
|
||||
group,
|
||||
|
@ -138,21 +162,44 @@ async function main() {
|
|||
0.0005,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(mangoAccount.toString(group));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (false) {
|
||||
// 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')!,
|
||||
);
|
||||
const lowestAsk = Array.from(asks!)[0];
|
||||
const bids = await group.loadSerum3BidsForMarket(
|
||||
client,
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
const highestBid = Array.from(asks!)![0];
|
||||
|
||||
let price = 20;
|
||||
let qty = 0.0001;
|
||||
console.log(
|
||||
`...placing serum3 bid which would not be settled since its relatively low then midprice`,
|
||||
`...placing serum3 bid which would not be settled since its relatively low then midprice at ${price} for ${qty}`,
|
||||
);
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
Serum3Side.bid,
|
||||
20,
|
||||
0.0001,
|
||||
price,
|
||||
qty,
|
||||
Serum3SelfTradeBehavior.decrementTake,
|
||||
Serum3OrderType.limit,
|
||||
Date.now(),
|
||||
|
@ -160,15 +207,18 @@ async function main() {
|
|||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...placing serum3 bid way above midprice`);
|
||||
price = lowestAsk.price + lowestAsk.price / 2;
|
||||
qty = 0.0001;
|
||||
console.log(
|
||||
`...placing serum3 bid way above midprice at ${price} for ${qty}`,
|
||||
);
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
Serum3Side.bid,
|
||||
90000,
|
||||
0.0001,
|
||||
price,
|
||||
qty,
|
||||
Serum3SelfTradeBehavior.decrementTake,
|
||||
Serum3OrderType.limit,
|
||||
Date.now(),
|
||||
|
@ -176,15 +226,18 @@ async function main() {
|
|||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...placing serum3 ask way below midprice`);
|
||||
price = highestBid.price - highestBid.price / 2;
|
||||
qty = 0.0001;
|
||||
console.log(
|
||||
`...placing serum3 ask way below midprice at ${price} for ${qty}`,
|
||||
);
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
Serum3Side.ask,
|
||||
30000,
|
||||
0.0001,
|
||||
price,
|
||||
qty,
|
||||
Serum3SelfTradeBehavior.decrementTake,
|
||||
Serum3OrderType.limit,
|
||||
Date.now(),
|
||||
|
@ -192,10 +245,10 @@ async function main() {
|
|||
);
|
||||
|
||||
console.log(`...current own orders on OB`);
|
||||
let orders = await client.getSerum3Orders(
|
||||
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
|
||||
client,
|
||||
group,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(
|
||||
|
@ -205,18 +258,17 @@ async function main() {
|
|||
await client.serum3CancelOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
|
||||
order.orderId,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`...current own orders on OB`);
|
||||
orders = await client.getSerum3Orders(
|
||||
orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
|
||||
client,
|
||||
group,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(order);
|
||||
|
@ -226,12 +278,19 @@ async function main() {
|
|||
await client.serum3SettleFunds(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
}
|
||||
|
||||
if (true) {
|
||||
if (false) {
|
||||
// serum3 market
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!.toBase58(),
|
||||
);
|
||||
console.log(await serum3Market?.logOb(client, group));
|
||||
}
|
||||
|
||||
if (false) {
|
||||
await mangoAccount.reload(client, group);
|
||||
console.log(
|
||||
'...mangoAccount.getEquity() ' +
|
||||
|
@ -252,13 +311,13 @@ async function main() {
|
|||
console.log(
|
||||
'...mangoAccount.getAssetsVal() ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getAssetsVal(HealthType.init).toNumber(),
|
||||
mangoAccount.getAssetsValue(HealthType.init).toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'...mangoAccount.getLiabsVal() ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getLiabsVal(HealthType.init).toNumber(),
|
||||
mangoAccount.getLiabsValue(HealthType.init).toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
|
@ -272,14 +331,80 @@ async function main() {
|
|||
).toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
"...mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount
|
||||
.getSerum3MarketMarginAvailable(group, 'BTC/USDC')
|
||||
.toNumber(),
|
||||
),
|
||||
}
|
||||
|
||||
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
|
||||
.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(),
|
||||
);
|
||||
}
|
||||
for (const srcToken of Array.from(group.banksMapByName.keys())) {
|
||||
for (const tgtToken of Array.from(group.banksMapByName.keys())) {
|
||||
getMaxSourceForTokenSwapWrapper(srcToken, tgtToken);
|
||||
}
|
||||
}
|
||||
|
||||
const maxQuoteForSerum3BidUi = mangoAccount.getMaxQuoteForSerum3BidUi(
|
||||
group,
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
console.log(
|
||||
"...mangoAccount.getMaxQuoteForSerum3BidUi(group, 'BTC/USDC') " +
|
||||
maxQuoteForSerum3BidUi,
|
||||
);
|
||||
|
||||
const maxBaseForSerum3AskUi = mangoAccount.getMaxBaseForSerum3AskUi(
|
||||
group,
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
console.log(
|
||||
"...mangoAccount.getMaxBaseForSerum3AskUi(group, 'BTC/USDC') " +
|
||||
maxBaseForSerum3AskUi,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`simHealthRatioWithSerum3BidUiChanges ${mangoAccount.simHealthRatioWithSerum3BidUiChanges(
|
||||
group,
|
||||
785,
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
)}`,
|
||||
);
|
||||
console.log(
|
||||
`simHealthRatioWithSerum3AskUiChanges ${mangoAccount.simHealthRatioWithSerum3AskUiChanges(
|
||||
group,
|
||||
0.033,
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (false) {
|
||||
console.log(
|
||||
"...mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " +
|
||||
toUiDecimalsForQuote(
|
||||
|
@ -290,7 +415,7 @@ async function main() {
|
|||
);
|
||||
}
|
||||
|
||||
if (true) {
|
||||
if (false) {
|
||||
// perps
|
||||
console.log(`...placing perp bid`);
|
||||
try {
|
||||
|
|
|
@ -9,8 +9,11 @@ import {
|
|||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
import { QUOTE_DECIMALS } from './accounts/bank';
|
||||
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';
|
||||
|
||||
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
|
||||
|
||||
|
@ -26,6 +29,59 @@ export function debugAccountMetas(ams: AccountMeta[]) {
|
|||
}
|
||||
}
|
||||
|
||||
export function debugHealthAccounts(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
publicKeys: PublicKey[],
|
||||
) {
|
||||
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.perpMarketsMap.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 async function findOrCreate<T>(
|
||||
entityName: string,
|
||||
findMethod: (...x: any) => any,
|
||||
|
|
Loading…
Reference in New Issue