2022-07-31 00:25:11 -07:00
|
|
|
use std::str::FromStr;
|
2022-07-16 05:37:15 -07:00
|
|
|
use std::sync::Arc;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
use anchor_client::{Client, ClientError, Cluster, Program};
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
use anchor_lang::__private::bytemuck;
|
|
|
|
use anchor_lang::prelude::System;
|
2022-07-16 05:37:15 -07:00
|
|
|
use anchor_lang::Id;
|
2022-05-27 22:05:34 -07:00
|
|
|
use anchor_spl::associated_token::get_associated_token_address;
|
2022-07-16 05:37:15 -07:00
|
|
|
use anchor_spl::token::Token;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-06-18 07:31:28 -07:00
|
|
|
use fixed::types::I80F48;
|
|
|
|
use itertools::Itertools;
|
2022-05-27 22:05:34 -07:00
|
|
|
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
|
2022-07-25 07:07:53 -07:00
|
|
|
use mango_v4::state::{AccountSize, Bank, Group, MangoAccountValue, Serum3MarketIndex, TokenIndex};
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
use solana_client::rpc_client::RpcClient;
|
2022-07-31 00:25:11 -07:00
|
|
|
use solana_sdk::signer::keypair;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
use crate::account_fetcher::*;
|
|
|
|
use crate::context::{MangoGroupContext, Serum3MarketContext, TokenContext};
|
|
|
|
use crate::gpa::fetch_mango_accounts;
|
2022-05-29 03:25:12 -07:00
|
|
|
use crate::util::MyClone;
|
2022-07-16 05:37:15 -07:00
|
|
|
|
2022-05-29 03:25:12 -07:00
|
|
|
use anyhow::Context;
|
2022-05-27 22:05:34 -07:00
|
|
|
use solana_sdk::instruction::{AccountMeta, Instruction};
|
|
|
|
use solana_sdk::signature::{Keypair, Signature};
|
|
|
|
use solana_sdk::sysvar;
|
|
|
|
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signer::Signer};
|
|
|
|
|
2022-06-19 11:16:12 -07:00
|
|
|
// todo: might want to integrate geyser, websockets, or simple http polling for keeping data fresh
|
2022-05-27 22:05:34 -07:00
|
|
|
pub struct MangoClient {
|
|
|
|
pub rpc: RpcClient,
|
|
|
|
pub cluster: Cluster,
|
|
|
|
pub commitment: CommitmentConfig,
|
2022-07-16 05:37:15 -07:00
|
|
|
|
|
|
|
// todo: possibly this object should have cache-functions, so there can be one getMultipleAccounts
|
|
|
|
// call to refresh banks etc -- if it's backed by websockets, these could just do nothing
|
|
|
|
pub account_fetcher: Arc<dyn AccountFetcher>,
|
|
|
|
|
2022-05-27 22:05:34 -07:00
|
|
|
pub payer: Keypair,
|
2022-07-16 05:37:15 -07:00
|
|
|
pub mango_account_address: Pubkey,
|
|
|
|
|
|
|
|
pub context: MangoGroupContext,
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: add retry framework for sending tx and rpc calls
|
|
|
|
// 1/ this works right now, but I think mid-term the MangoClient will want to interact with multiple mango accounts
|
|
|
|
// -- then we should probably specify accounts by owner+account_num / or pubkey
|
|
|
|
// 2/ pubkey, can be both owned, but also delegated accouns
|
|
|
|
|
|
|
|
impl MangoClient {
|
2022-07-14 03:57:19 -07:00
|
|
|
pub fn group_for_admin(admin: Pubkey, num: u32) -> Pubkey {
|
|
|
|
Pubkey::find_program_address(
|
|
|
|
&["Group".as_ref(), admin.as_ref(), num.to_le_bytes().as_ref()],
|
|
|
|
&mango_v4::ID,
|
|
|
|
)
|
|
|
|
.0
|
|
|
|
}
|
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
/// Conveniently creates a RPC based client
|
2022-05-27 22:05:34 -07:00
|
|
|
pub fn new(
|
|
|
|
cluster: Cluster,
|
|
|
|
commitment: CommitmentConfig,
|
2022-07-14 03:57:19 -07:00
|
|
|
group: Pubkey,
|
2022-07-16 05:37:15 -07:00
|
|
|
payer: Keypair,
|
2022-06-18 07:31:28 -07:00
|
|
|
mango_account_name: &str,
|
2022-07-16 05:37:15 -07:00
|
|
|
) -> anyhow::Result<Self> {
|
|
|
|
let group_context = MangoGroupContext::new_from_rpc(group, cluster.clone(), commitment)?;
|
|
|
|
|
|
|
|
let rpc = RpcClient::new_with_commitment(cluster.url().to_string(), commitment);
|
|
|
|
let account_fetcher = Arc::new(CachedAccountFetcher::new(RpcAccountFetcher { rpc }));
|
|
|
|
|
|
|
|
Self::new_detail(
|
|
|
|
cluster,
|
|
|
|
commitment,
|
|
|
|
payer,
|
|
|
|
mango_account_name,
|
|
|
|
group_context,
|
|
|
|
account_fetcher,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Allows control of AccountFetcher and externally created MangoGroupContext
|
|
|
|
pub fn new_detail(
|
|
|
|
cluster: Cluster,
|
|
|
|
commitment: CommitmentConfig,
|
|
|
|
payer: Keypair,
|
|
|
|
mango_account_name: &str,
|
|
|
|
// future: maybe pass Arc<MangoGroupContext>, so it can be extenally updated?
|
|
|
|
group_context: MangoGroupContext,
|
|
|
|
account_fetcher: Arc<dyn AccountFetcher>,
|
2022-05-27 22:05:34 -07:00
|
|
|
) -> anyhow::Result<Self> {
|
|
|
|
let program =
|
|
|
|
Client::new_with_options(cluster.clone(), std::rc::Rc::new(payer.clone()), commitment)
|
|
|
|
.program(mango_v4::ID);
|
|
|
|
|
|
|
|
let rpc = program.rpc();
|
2022-07-16 05:37:15 -07:00
|
|
|
let group = group_context.group;
|
2022-07-13 05:04:20 -07:00
|
|
|
|
2022-05-29 03:25:12 -07:00
|
|
|
// Mango Account
|
2022-07-16 05:37:15 -07:00
|
|
|
let mut mango_account_tuples = fetch_mango_accounts(&program, group, payer.pubkey())?;
|
2022-05-29 03:25:12 -07:00
|
|
|
let mango_account_opt = mango_account_tuples
|
|
|
|
.iter()
|
2022-07-25 07:07:53 -07:00
|
|
|
.find(|(_, account)| account.fixed.name() == mango_account_name);
|
2022-05-29 03:25:12 -07:00
|
|
|
if mango_account_opt.is_none() {
|
2022-07-25 07:07:53 -07:00
|
|
|
mango_account_tuples.sort_by(|a, b| {
|
|
|
|
a.1.fixed
|
|
|
|
.account_num
|
|
|
|
.partial_cmp(&b.1.fixed.account_num)
|
|
|
|
.unwrap()
|
|
|
|
});
|
2022-05-29 03:25:12 -07:00
|
|
|
let account_num = match mango_account_tuples.last() {
|
2022-07-25 07:07:53 -07:00
|
|
|
Some(tuple) => tuple.1.fixed.account_num + 1,
|
2022-08-01 09:46:45 -07:00
|
|
|
None => 0u32,
|
2022-05-29 03:25:12 -07:00
|
|
|
};
|
|
|
|
program
|
|
|
|
.request()
|
|
|
|
.instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
2022-07-06 05:51:15 -07:00
|
|
|
&mango_v4::accounts::AccountCreate {
|
2022-05-29 03:25:12 -07:00
|
|
|
group,
|
|
|
|
owner: payer.pubkey(),
|
|
|
|
account: {
|
|
|
|
Pubkey::find_program_address(
|
|
|
|
&[
|
|
|
|
group.as_ref(),
|
|
|
|
b"MangoAccount".as_ref(),
|
|
|
|
payer.pubkey().as_ref(),
|
|
|
|
&account_num.to_le_bytes(),
|
|
|
|
],
|
|
|
|
&mango_v4::id(),
|
|
|
|
)
|
|
|
|
.0
|
|
|
|
},
|
|
|
|
payer: payer.pubkey(),
|
|
|
|
system_program: System::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
),
|
|
|
|
data: anchor_lang::InstructionData::data(
|
2022-07-06 05:51:15 -07:00
|
|
|
&mango_v4::instruction::AccountCreate {
|
2022-05-29 03:25:12 -07:00
|
|
|
account_num,
|
|
|
|
name: mango_account_name.to_owned(),
|
2022-07-25 07:07:53 -07:00
|
|
|
account_size: AccountSize::Small,
|
2022-05-29 03:25:12 -07:00
|
|
|
},
|
|
|
|
),
|
|
|
|
})
|
|
|
|
.send()
|
2022-07-31 00:25:11 -07:00
|
|
|
.map_err(prettify_client_error)
|
2022-05-29 03:25:12 -07:00
|
|
|
.context("Failed to create account...")?;
|
|
|
|
}
|
2022-07-16 05:37:15 -07:00
|
|
|
let mango_account_tuples = fetch_mango_accounts(&program, group, payer.pubkey())?;
|
2022-05-29 03:25:12 -07:00
|
|
|
let index = mango_account_tuples
|
|
|
|
.iter()
|
2022-07-25 07:07:53 -07:00
|
|
|
.position(|tuple| tuple.1.fixed.name() == mango_account_name)
|
2022-05-29 03:25:12 -07:00
|
|
|
.unwrap();
|
2022-07-25 07:07:53 -07:00
|
|
|
let mango_account_cache = &mango_account_tuples[index];
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
rpc,
|
2022-07-19 00:59:30 -07:00
|
|
|
cluster,
|
2022-05-27 22:05:34 -07:00
|
|
|
commitment,
|
2022-07-16 05:37:15 -07:00
|
|
|
account_fetcher,
|
2022-05-27 22:05:34 -07:00
|
|
|
payer,
|
2022-07-16 05:37:15 -07:00
|
|
|
mango_account_address: mango_account_cache.0,
|
|
|
|
context: group_context,
|
2022-05-27 22:05:34 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn client(&self) -> Client {
|
|
|
|
Client::new_with_options(
|
|
|
|
self.cluster.clone(),
|
|
|
|
std::rc::Rc::new(self.payer.clone()),
|
|
|
|
self.commitment,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn program(&self) -> Program {
|
|
|
|
self.client().program(mango_v4::ID)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn payer(&self) -> Pubkey {
|
|
|
|
self.payer.pubkey()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn group(&self) -> Pubkey {
|
2022-07-16 05:37:15 -07:00
|
|
|
self.context.group
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
2022-07-25 07:07:53 -07:00
|
|
|
pub fn mango_account(&self) -> anyhow::Result<MangoAccountValue> {
|
|
|
|
account_fetcher_fetch_mango_account(&*self.account_fetcher, self.mango_account_address)
|
2022-07-16 05:37:15 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn first_bank(&self, token_index: TokenIndex) -> anyhow::Result<Bank> {
|
|
|
|
let bank_address = self.context.mint_info(token_index).first_bank();
|
|
|
|
account_fetcher_fetch_anchor_account(&*self.account_fetcher, bank_address)
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn derive_health_check_remaining_account_metas(
|
|
|
|
&self,
|
2022-07-16 05:37:15 -07:00
|
|
|
affected_token: Option<TokenIndex>,
|
2022-05-27 22:05:34 -07:00
|
|
|
writable_banks: bool,
|
2022-07-16 05:37:15 -07:00
|
|
|
) -> anyhow::Result<Vec<AccountMeta>> {
|
|
|
|
let account = self.mango_account()?;
|
|
|
|
self.context.derive_health_check_remaining_account_metas(
|
|
|
|
&account,
|
|
|
|
affected_token,
|
|
|
|
writable_banks,
|
|
|
|
)
|
2022-06-18 07:31:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn derive_liquidation_health_check_remaining_account_metas(
|
|
|
|
&self,
|
2022-07-25 07:07:53 -07:00
|
|
|
liqee: &MangoAccountValue,
|
2022-06-18 07:31:28 -07:00
|
|
|
asset_token_index: TokenIndex,
|
|
|
|
liab_token_index: TokenIndex,
|
2022-07-16 05:37:15 -07:00
|
|
|
) -> anyhow::Result<Vec<AccountMeta>> {
|
2022-06-18 07:31:28 -07:00
|
|
|
// figure out all the banks/oracles that need to be passed for the health check
|
|
|
|
let mut banks = vec![];
|
|
|
|
let mut oracles = vec![];
|
2022-07-16 05:37:15 -07:00
|
|
|
let account = self.mango_account()?;
|
2022-06-18 07:31:28 -07:00
|
|
|
|
|
|
|
let token_indexes = liqee
|
2022-07-25 07:07:53 -07:00
|
|
|
.token_iter_active()
|
|
|
|
.chain(account.token_iter_active())
|
2022-06-18 07:31:28 -07:00
|
|
|
.map(|ta| ta.token_index)
|
|
|
|
.unique();
|
|
|
|
|
|
|
|
for token_index in token_indexes {
|
2022-07-16 05:37:15 -07:00
|
|
|
let mint_info = self.context.mint_info(token_index);
|
2022-06-18 07:31:28 -07:00
|
|
|
let writable_bank = token_index == asset_token_index || token_index == liab_token_index;
|
2022-06-27 02:27:17 -07:00
|
|
|
banks.push((mint_info.first_bank(), writable_bank));
|
2022-06-18 07:31:28 -07:00
|
|
|
oracles.push(mint_info.oracle);
|
|
|
|
}
|
|
|
|
|
|
|
|
let serum_oos = liqee
|
2022-07-25 07:07:53 -07:00
|
|
|
.serum3_iter_active()
|
|
|
|
.chain(account.serum3_iter_active())
|
2022-06-18 07:31:28 -07:00
|
|
|
.map(|&s| s.open_orders);
|
|
|
|
let perp_markets = liqee
|
2022-07-25 07:07:53 -07:00
|
|
|
.perp_iter_active_accounts()
|
|
|
|
.chain(account.perp_iter_active_accounts())
|
2022-07-16 05:37:15 -07:00
|
|
|
.map(|&pa| self.context.perp_market_address(pa.market_index));
|
2022-06-18 07:31:28 -07:00
|
|
|
|
|
|
|
let to_account_meta = |pubkey| AccountMeta {
|
|
|
|
pubkey,
|
|
|
|
is_writable: false,
|
|
|
|
is_signer: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(banks
|
|
|
|
.iter()
|
|
|
|
.map(|(pubkey, is_writable)| AccountMeta {
|
|
|
|
pubkey: *pubkey,
|
|
|
|
is_writable: *is_writable,
|
|
|
|
is_signer: false,
|
|
|
|
})
|
|
|
|
.chain(oracles.into_iter().map(to_account_meta))
|
|
|
|
.chain(perp_markets.map(to_account_meta))
|
2022-07-16 05:37:15 -07:00
|
|
|
.chain(serum_oos.map(to_account_meta))
|
2022-05-27 22:05:34 -07:00
|
|
|
.collect())
|
|
|
|
}
|
|
|
|
|
2022-07-15 01:00:01 -07:00
|
|
|
pub fn token_deposit(&self, token_name: &str, amount: u64) -> anyhow::Result<Signature> {
|
2022-07-16 05:37:15 -07:00
|
|
|
let token_index = *self.context.token_indexes_by_name.get(token_name).unwrap();
|
|
|
|
let mint_info = self.context.mint_info(token_index);
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
let health_check_metas =
|
2022-07-16 05:37:15 -07:00
|
|
|
self.derive_health_check_remaining_account_metas(Some(token_index), false)?;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
self.program()
|
|
|
|
.request()
|
|
|
|
.instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: {
|
|
|
|
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
2022-06-09 09:27:31 -07:00
|
|
|
&mango_v4::accounts::TokenDeposit {
|
2022-05-27 22:05:34 -07:00
|
|
|
group: self.group(),
|
2022-07-16 05:37:15 -07:00
|
|
|
account: self.mango_account_address,
|
|
|
|
bank: mint_info.first_bank(),
|
|
|
|
vault: mint_info.first_vault(),
|
2022-05-27 22:05:34 -07:00
|
|
|
token_account: get_associated_token_address(
|
|
|
|
&self.payer(),
|
|
|
|
&mint_info.mint,
|
|
|
|
),
|
|
|
|
token_authority: self.payer(),
|
|
|
|
token_program: Token::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
);
|
|
|
|
ams.extend(health_check_metas.into_iter());
|
|
|
|
ams
|
|
|
|
},
|
2022-06-09 09:27:31 -07:00
|
|
|
data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenDeposit {
|
2022-05-27 22:05:34 -07:00
|
|
|
amount,
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
.send()
|
2022-07-15 01:00:01 -07:00
|
|
|
.map_err(prettify_client_error)
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_oracle_price(
|
|
|
|
&self,
|
|
|
|
token_name: &str,
|
|
|
|
) -> Result<pyth_sdk_solana::Price, anyhow::Error> {
|
2022-07-16 05:37:15 -07:00
|
|
|
let token_index = *self.context.token_indexes_by_name.get(token_name).unwrap();
|
|
|
|
let mint_info = self.context.mint_info(token_index);
|
|
|
|
let oracle_account = self.account_fetcher.fetch_raw_account(mint_info.oracle)?;
|
|
|
|
Ok(pyth_sdk_solana::load_price(&oracle_account.data).unwrap())
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Serum3
|
|
|
|
//
|
|
|
|
|
2022-07-15 01:00:01 -07:00
|
|
|
pub fn serum3_create_open_orders(&self, name: &str) -> anyhow::Result<Signature> {
|
2022-07-16 05:37:15 -07:00
|
|
|
let account_pubkey = self.mango_account_address;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
let market_index = *self
|
|
|
|
.context
|
|
|
|
.serum3_market_indexes_by_name
|
|
|
|
.get(name)
|
|
|
|
.unwrap();
|
|
|
|
let serum3_info = self.context.serum3_markets.get(&market_index).unwrap();
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
let open_orders = Pubkey::find_program_address(
|
|
|
|
&[
|
|
|
|
account_pubkey.as_ref(),
|
|
|
|
b"Serum3OO".as_ref(),
|
2022-07-16 05:37:15 -07:00
|
|
|
serum3_info.address.as_ref(),
|
2022-05-27 22:05:34 -07:00
|
|
|
],
|
|
|
|
&self.program().id(),
|
|
|
|
)
|
|
|
|
.0;
|
|
|
|
|
|
|
|
self.program()
|
|
|
|
.request()
|
|
|
|
.instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
|
|
|
&mango_v4::accounts::Serum3CreateOpenOrders {
|
|
|
|
group: self.group(),
|
|
|
|
account: account_pubkey,
|
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
serum_market: serum3_info.address,
|
|
|
|
serum_program: serum3_info.market.serum_program,
|
|
|
|
serum_market_external: serum3_info.market.serum_market_external,
|
2022-05-27 22:05:34 -07:00
|
|
|
open_orders,
|
|
|
|
owner: self.payer(),
|
|
|
|
payer: self.payer(),
|
|
|
|
system_program: System::id(),
|
|
|
|
rent: sysvar::rent::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
),
|
|
|
|
data: anchor_lang::InstructionData::data(
|
|
|
|
&mango_v4::instruction::Serum3CreateOpenOrders {},
|
|
|
|
),
|
|
|
|
})
|
|
|
|
.send()
|
2022-07-15 01:00:01 -07:00
|
|
|
.map_err(prettify_client_error)
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
fn serum3_data<'a>(&'a self, name: &str) -> Result<Serum3Data<'a>, ClientError> {
|
|
|
|
let market_index = *self
|
|
|
|
.context
|
|
|
|
.serum3_market_indexes_by_name
|
|
|
|
.get(name)
|
|
|
|
.unwrap();
|
|
|
|
let serum3_info = self.context.serum3_markets.get(&market_index).unwrap();
|
|
|
|
|
|
|
|
let quote_info = self.context.token(serum3_info.market.quote_token_index);
|
|
|
|
let base_info = self.context.token(serum3_info.market.base_token_index);
|
|
|
|
|
|
|
|
Ok(Serum3Data {
|
|
|
|
market_index,
|
|
|
|
market: serum3_info,
|
|
|
|
quote: quote_info,
|
|
|
|
base: base_info,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-27 22:05:34 -07:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
pub fn serum3_place_order(
|
|
|
|
&self,
|
|
|
|
name: &str,
|
|
|
|
side: Serum3Side,
|
|
|
|
price: f64,
|
|
|
|
size: f64,
|
|
|
|
self_trade_behavior: Serum3SelfTradeBehavior,
|
|
|
|
order_type: Serum3OrderType,
|
|
|
|
client_order_id: u64,
|
|
|
|
limit: u16,
|
2022-07-15 01:00:01 -07:00
|
|
|
) -> anyhow::Result<Signature> {
|
2022-07-16 05:37:15 -07:00
|
|
|
let s3 = self.serum3_data(name)?;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
let account = self.mango_account()?;
|
2022-07-25 07:07:53 -07:00
|
|
|
let open_orders = account.serum3_find(s3.market_index).unwrap().open_orders;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
let health_check_metas = self.derive_health_check_remaining_account_metas(None, false)?;
|
|
|
|
|
|
|
|
// https://github.com/project-serum/serum-ts/blob/master/packages/serum/src/market.ts#L1306
|
|
|
|
let limit_price = {
|
2022-07-16 05:37:15 -07:00
|
|
|
(price * ((10u64.pow(s3.quote.decimals as u32) * s3.market.coin_lot_size) as f64))
|
2022-05-27 22:05:34 -07:00
|
|
|
as u64
|
2022-07-16 05:37:15 -07:00
|
|
|
/ (10u64.pow(s3.base.decimals as u32) * s3.market.pc_lot_size)
|
2022-05-27 22:05:34 -07:00
|
|
|
};
|
|
|
|
// https://github.com/project-serum/serum-ts/blob/master/packages/serum/src/market.ts#L1333
|
2022-07-16 05:37:15 -07:00
|
|
|
let max_base_qty =
|
|
|
|
{ (size * 10u64.pow(s3.base.decimals as u32) as f64) as u64 / s3.market.coin_lot_size };
|
2022-05-27 22:05:34 -07:00
|
|
|
let max_native_quote_qty_including_fees = {
|
|
|
|
fn get_fee_tier(msrm_balance: u64, srm_balance: u64) -> u64 {
|
|
|
|
if msrm_balance >= 1 {
|
|
|
|
6
|
|
|
|
} else if srm_balance >= 1_000_000 {
|
|
|
|
5
|
|
|
|
} else if srm_balance >= 100_000 {
|
|
|
|
4
|
|
|
|
} else if srm_balance >= 10_000 {
|
|
|
|
3
|
|
|
|
} else if srm_balance >= 1_000 {
|
|
|
|
2
|
|
|
|
} else if srm_balance >= 100 {
|
|
|
|
1
|
|
|
|
} else {
|
|
|
|
0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_fee_rates(fee_tier: u64) -> (f64, f64) {
|
|
|
|
if fee_tier == 1 {
|
|
|
|
// SRM2
|
|
|
|
return (0.002, -0.0003);
|
|
|
|
} else if fee_tier == 2 {
|
|
|
|
// SRM3
|
|
|
|
return (0.0018, -0.0003);
|
|
|
|
} else if fee_tier == 3 {
|
|
|
|
// SRM4
|
|
|
|
return (0.0016, -0.0003);
|
|
|
|
} else if fee_tier == 4 {
|
|
|
|
// SRM5
|
|
|
|
return (0.0014, -0.0003);
|
|
|
|
} else if fee_tier == 5 {
|
|
|
|
// SRM6
|
|
|
|
return (0.0012, -0.0003);
|
|
|
|
} else if fee_tier == 6 {
|
|
|
|
// MSRM
|
|
|
|
return (0.001, -0.0005);
|
|
|
|
}
|
|
|
|
// Base
|
|
|
|
(0.0022, -0.0003)
|
|
|
|
}
|
|
|
|
|
|
|
|
let fee_tier = get_fee_tier(0, 0);
|
|
|
|
let rates = get_fee_rates(fee_tier);
|
2022-07-16 05:37:15 -07:00
|
|
|
(s3.market.pc_lot_size as f64 * (1f64 + rates.0)) as u64 * (limit_price * max_base_qty)
|
2022-05-27 22:05:34 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
self.program()
|
|
|
|
.request()
|
|
|
|
.instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: {
|
|
|
|
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
|
|
|
&mango_v4::accounts::Serum3PlaceOrder {
|
|
|
|
group: self.group(),
|
2022-07-16 05:37:15 -07:00
|
|
|
account: self.mango_account_address,
|
2022-05-27 22:05:34 -07:00
|
|
|
open_orders,
|
2022-07-16 05:37:15 -07:00
|
|
|
quote_bank: s3.quote.mint_info.first_bank(),
|
|
|
|
quote_vault: s3.quote.mint_info.first_vault(),
|
|
|
|
base_bank: s3.base.mint_info.first_bank(),
|
|
|
|
base_vault: s3.base.mint_info.first_vault(),
|
|
|
|
serum_market: s3.market.address,
|
|
|
|
serum_program: s3.market.market.serum_program,
|
|
|
|
serum_market_external: s3.market.market.serum_market_external,
|
|
|
|
market_bids: s3.market.bids,
|
|
|
|
market_asks: s3.market.asks,
|
|
|
|
market_event_queue: s3.market.event_q,
|
|
|
|
market_request_queue: s3.market.req_q,
|
|
|
|
market_base_vault: s3.market.coin_vault,
|
|
|
|
market_quote_vault: s3.market.pc_vault,
|
|
|
|
market_vault_signer: s3.market.vault_signer,
|
2022-05-27 22:05:34 -07:00
|
|
|
owner: self.payer(),
|
|
|
|
token_program: Token::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
);
|
|
|
|
ams.extend(health_check_metas.into_iter());
|
|
|
|
ams
|
|
|
|
},
|
|
|
|
data: anchor_lang::InstructionData::data(
|
|
|
|
&mango_v4::instruction::Serum3PlaceOrder {
|
|
|
|
side,
|
|
|
|
limit_price,
|
|
|
|
max_base_qty,
|
|
|
|
max_native_quote_qty_including_fees,
|
|
|
|
self_trade_behavior,
|
|
|
|
order_type,
|
|
|
|
client_order_id,
|
|
|
|
limit,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
})
|
|
|
|
.send()
|
2022-07-15 01:00:01 -07:00
|
|
|
.map_err(prettify_client_error)
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
2022-07-15 01:00:01 -07:00
|
|
|
pub fn serum3_settle_funds(&self, name: &str) -> anyhow::Result<Signature> {
|
2022-07-16 05:37:15 -07:00
|
|
|
let s3 = self.serum3_data(name)?;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
let account = self.mango_account()?;
|
2022-07-25 07:07:53 -07:00
|
|
|
let open_orders = account.serum3_find(s3.market_index).unwrap().open_orders;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
self.program()
|
|
|
|
.request()
|
|
|
|
.instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
|
|
|
&mango_v4::accounts::Serum3SettleFunds {
|
|
|
|
group: self.group(),
|
2022-07-16 05:37:15 -07:00
|
|
|
account: self.mango_account_address,
|
2022-05-27 22:05:34 -07:00
|
|
|
open_orders,
|
2022-07-16 05:37:15 -07:00
|
|
|
quote_bank: s3.quote.mint_info.first_bank(),
|
|
|
|
quote_vault: s3.quote.mint_info.first_vault(),
|
|
|
|
base_bank: s3.base.mint_info.first_bank(),
|
|
|
|
base_vault: s3.base.mint_info.first_vault(),
|
|
|
|
serum_market: s3.market.address,
|
|
|
|
serum_program: s3.market.market.serum_program,
|
|
|
|
serum_market_external: s3.market.market.serum_market_external,
|
|
|
|
market_base_vault: s3.market.coin_vault,
|
|
|
|
market_quote_vault: s3.market.pc_vault,
|
|
|
|
market_vault_signer: s3.market.vault_signer,
|
2022-05-27 22:05:34 -07:00
|
|
|
owner: self.payer(),
|
|
|
|
token_program: Token::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
),
|
|
|
|
data: anchor_lang::InstructionData::data(
|
|
|
|
&mango_v4::instruction::Serum3SettleFunds {},
|
|
|
|
),
|
|
|
|
})
|
|
|
|
.send()
|
2022-07-15 01:00:01 -07:00
|
|
|
.map_err(prettify_client_error)
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn serum3_cancel_all_orders(&self, market_name: &str) -> Result<Vec<u128>, anyhow::Error> {
|
2022-07-16 05:37:15 -07:00
|
|
|
let market_index = *self
|
|
|
|
.context
|
|
|
|
.serum3_market_indexes_by_name
|
|
|
|
.get(market_name)
|
|
|
|
.unwrap();
|
|
|
|
let account = self.mango_account()?;
|
2022-07-25 07:07:53 -07:00
|
|
|
let open_orders = account.serum3_find(market_index).unwrap().open_orders;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
let open_orders_bytes = self.account_fetcher.fetch_raw_account(open_orders)?.data;
|
2022-05-27 22:05:34 -07:00
|
|
|
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 mut orders = vec![];
|
|
|
|
for order_id in open_orders_data.orders {
|
|
|
|
if order_id != 0 {
|
|
|
|
// TODO: find side for order_id, and only cancel the relevant order
|
|
|
|
self.serum3_cancel_order(market_name, Serum3Side::Bid, order_id)
|
|
|
|
.ok();
|
|
|
|
self.serum3_cancel_order(market_name, Serum3Side::Ask, order_id)
|
|
|
|
.ok();
|
|
|
|
|
|
|
|
orders.push(order_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(orders)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn serum3_cancel_order(
|
|
|
|
&self,
|
|
|
|
market_name: &str,
|
|
|
|
side: Serum3Side,
|
|
|
|
order_id: u128,
|
2022-07-15 01:00:01 -07:00
|
|
|
) -> anyhow::Result<()> {
|
2022-07-16 05:37:15 -07:00
|
|
|
let s3 = self.serum3_data(market_name)?;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
let account = self.mango_account()?;
|
2022-07-25 07:07:53 -07:00
|
|
|
let open_orders = account.serum3_find(s3.market_index).unwrap().open_orders;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
self.program()
|
|
|
|
.request()
|
|
|
|
.instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: {
|
|
|
|
anchor_lang::ToAccountMetas::to_account_metas(
|
|
|
|
&mango_v4::accounts::Serum3CancelOrder {
|
|
|
|
group: self.group(),
|
2022-07-16 05:37:15 -07:00
|
|
|
account: self.mango_account_address,
|
|
|
|
serum_market: s3.market.address,
|
|
|
|
serum_program: s3.market.market.serum_program,
|
|
|
|
serum_market_external: s3.market.market.serum_market_external,
|
2022-05-27 22:05:34 -07:00
|
|
|
open_orders,
|
2022-07-16 05:37:15 -07:00
|
|
|
market_bids: s3.market.bids,
|
|
|
|
market_asks: s3.market.asks,
|
|
|
|
market_event_queue: s3.market.event_q,
|
2022-05-27 22:05:34 -07:00
|
|
|
owner: self.payer(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
)
|
|
|
|
},
|
|
|
|
data: anchor_lang::InstructionData::data(
|
|
|
|
&mango_v4::instruction::Serum3CancelOrder { side, order_id },
|
|
|
|
),
|
|
|
|
})
|
2022-07-15 01:00:01 -07:00
|
|
|
.send()
|
|
|
|
.map_err(prettify_client_error)?;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Perps
|
|
|
|
//
|
|
|
|
|
|
|
|
//
|
2022-06-18 07:31:28 -07:00
|
|
|
// Liquidation
|
2022-05-27 22:05:34 -07:00
|
|
|
//
|
2022-06-18 07:31:28 -07:00
|
|
|
|
|
|
|
pub fn liq_token_with_token(
|
|
|
|
&self,
|
2022-07-25 07:07:53 -07:00
|
|
|
liqee: (&Pubkey, &MangoAccountValue),
|
2022-06-18 07:31:28 -07:00
|
|
|
asset_token_index: TokenIndex,
|
|
|
|
liab_token_index: TokenIndex,
|
|
|
|
max_liab_transfer: I80F48,
|
2022-07-15 01:00:01 -07:00
|
|
|
) -> anyhow::Result<Signature> {
|
2022-06-18 07:31:28 -07:00
|
|
|
let health_remaining_ams = self
|
|
|
|
.derive_liquidation_health_check_remaining_account_metas(
|
|
|
|
liqee.1,
|
|
|
|
asset_token_index,
|
|
|
|
liab_token_index,
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
self.program()
|
|
|
|
.request()
|
|
|
|
.instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: {
|
|
|
|
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
|
|
|
&mango_v4::accounts::LiqTokenWithToken {
|
|
|
|
group: self.group(),
|
|
|
|
liqee: *liqee.0,
|
2022-07-16 05:37:15 -07:00
|
|
|
liqor: self.mango_account_address,
|
2022-06-18 07:31:28 -07:00
|
|
|
liqor_owner: self.payer.pubkey(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
);
|
|
|
|
ams.extend(health_remaining_ams);
|
|
|
|
ams
|
|
|
|
},
|
|
|
|
data: anchor_lang::InstructionData::data(
|
|
|
|
&mango_v4::instruction::LiqTokenWithToken {
|
|
|
|
asset_token_index,
|
|
|
|
liab_token_index,
|
|
|
|
max_liab_transfer,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
})
|
|
|
|
.send()
|
2022-07-15 01:00:01 -07:00
|
|
|
.map_err(prettify_client_error)
|
2022-06-18 07:31:28 -07:00
|
|
|
}
|
2022-07-13 05:04:20 -07:00
|
|
|
|
|
|
|
pub fn liq_token_bankruptcy(
|
|
|
|
&self,
|
2022-07-25 07:07:53 -07:00
|
|
|
liqee: (&Pubkey, &MangoAccountValue),
|
2022-07-13 05:04:20 -07:00
|
|
|
liab_token_index: TokenIndex,
|
|
|
|
max_liab_transfer: I80F48,
|
2022-07-15 01:00:01 -07:00
|
|
|
) -> anyhow::Result<Signature> {
|
2022-07-13 05:04:20 -07:00
|
|
|
let quote_token_index = 0;
|
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
let quote_info = self.context.token(quote_token_index);
|
|
|
|
let liab_info = self.context.token(liab_token_index);
|
2022-07-13 05:04:20 -07:00
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
let bank_remaining_ams = liab_info
|
|
|
|
.mint_info
|
2022-07-13 05:04:20 -07:00
|
|
|
.banks()
|
|
|
|
.iter()
|
|
|
|
.map(|bank_pubkey| AccountMeta {
|
|
|
|
pubkey: *bank_pubkey,
|
|
|
|
is_signer: false,
|
|
|
|
is_writable: true,
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
let health_remaining_ams = self
|
|
|
|
.derive_liquidation_health_check_remaining_account_metas(
|
|
|
|
liqee.1,
|
|
|
|
quote_token_index,
|
|
|
|
liab_token_index,
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
let group = account_fetcher_fetch_anchor_account::<Group>(
|
|
|
|
&*self.account_fetcher,
|
|
|
|
self.context.group,
|
|
|
|
)?;
|
|
|
|
|
2022-07-13 05:04:20 -07:00
|
|
|
self.program()
|
|
|
|
.request()
|
|
|
|
.instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: {
|
|
|
|
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
|
|
|
&mango_v4::accounts::LiqTokenBankruptcy {
|
|
|
|
group: self.group(),
|
|
|
|
liqee: *liqee.0,
|
2022-07-16 05:37:15 -07:00
|
|
|
liqor: self.mango_account_address,
|
2022-07-13 05:04:20 -07:00
|
|
|
liqor_owner: self.payer.pubkey(),
|
2022-07-16 05:37:15 -07:00
|
|
|
liab_mint_info: liab_info.mint_info_address,
|
|
|
|
quote_vault: quote_info.mint_info.first_vault(),
|
|
|
|
insurance_vault: group.insurance_vault,
|
2022-07-13 05:04:20 -07:00
|
|
|
token_program: Token::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
);
|
|
|
|
ams.extend(bank_remaining_ams);
|
|
|
|
ams.extend(health_remaining_ams);
|
|
|
|
ams
|
|
|
|
},
|
|
|
|
data: anchor_lang::InstructionData::data(
|
|
|
|
&mango_v4::instruction::LiqTokenBankruptcy {
|
|
|
|
liab_token_index,
|
|
|
|
max_liab_transfer,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
})
|
|
|
|
.send()
|
2022-07-15 01:00:01 -07:00
|
|
|
.map_err(prettify_client_error)
|
2022-07-13 05:04:20 -07:00
|
|
|
}
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
struct Serum3Data<'a> {
|
|
|
|
market_index: Serum3MarketIndex,
|
|
|
|
market: &'a Serum3MarketContext,
|
|
|
|
quote: &'a TokenContext,
|
|
|
|
base: &'a TokenContext,
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
2022-07-15 01:00:01 -07:00
|
|
|
|
2022-07-19 05:56:26 -07:00
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
pub enum MangoClientError {
|
|
|
|
#[error("Transaction simulation error. Logs: {logs}")]
|
|
|
|
SendTransactionPreflightFailure { logs: String },
|
|
|
|
}
|
|
|
|
|
2022-07-15 01:00:01 -07:00
|
|
|
/// Do some manual unpacking on some ClientErrors
|
|
|
|
///
|
|
|
|
/// Unfortunately solana's RpcResponseError will very unhelpfully print [N log messages]
|
|
|
|
/// instead of showing the actual log messages. This unpacks the error to provide more useful
|
|
|
|
/// output.
|
2022-07-31 00:25:11 -07:00
|
|
|
pub fn prettify_client_error(err: anchor_client::ClientError) -> anyhow::Error {
|
2022-07-15 01:00:01 -07:00
|
|
|
use solana_client::client_error::ClientErrorKind;
|
|
|
|
use solana_client::rpc_request::{RpcError, RpcResponseErrorData};
|
|
|
|
match &err {
|
|
|
|
anchor_client::ClientError::SolanaClientError(c) => {
|
|
|
|
match c.kind() {
|
|
|
|
ClientErrorKind::RpcError(RpcError::RpcResponseError { data, .. }) => match data {
|
|
|
|
RpcResponseErrorData::SendTransactionPreflightFailure(s) => {
|
|
|
|
if let Some(logs) = s.logs.as_ref() {
|
2022-07-19 05:56:26 -07:00
|
|
|
return MangoClientError::SendTransactionPreflightFailure {
|
|
|
|
logs: logs.iter().join("; "),
|
|
|
|
}
|
|
|
|
.into();
|
2022-07-15 01:00:01 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
},
|
|
|
|
_ => {}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
};
|
|
|
|
err.into()
|
|
|
|
}
|
2022-07-31 00:25:11 -07:00
|
|
|
|
|
|
|
pub fn keypair_from_cli(keypair: &str) -> Keypair {
|
|
|
|
let maybe_keypair = keypair::read_keypair(&mut keypair.as_bytes());
|
|
|
|
match maybe_keypair {
|
|
|
|
Ok(keypair) => keypair,
|
|
|
|
Err(_) => {
|
|
|
|
let path = std::path::PathBuf::from_str(&*shellexpand::tilde(keypair)).unwrap();
|
|
|
|
keypair::read_keypair_file(path)
|
|
|
|
.unwrap_or_else(|_| panic!("Failed to read keypair from {}", keypair))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|