2022-08-01 05:19:52 -07:00
|
|
|
use std::rc::Rc;
|
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-08-01 05:19:52 -07:00
|
|
|
use std::time::Duration;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-08-01 05:19:52 -07:00
|
|
|
use anchor_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-08-04 08:01:00 -07:00
|
|
|
use bincode::Options;
|
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-08-07 05:16:06 -07:00
|
|
|
use mango_v4::state::{Bank, Group, MangoAccountValue, Serum3MarketIndex, TokenIndex};
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-08-04 08:01:00 -07:00
|
|
|
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
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-08-04 08:01:00 -07:00
|
|
|
use crate::jupiter;
|
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-08-01 05:19:52 -07:00
|
|
|
// very close to anchor_client::Client, which unfortunately has no accessors or Clone
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct Client {
|
2022-05-27 22:05:34 -07:00
|
|
|
pub cluster: Cluster,
|
2022-08-01 05:19:52 -07:00
|
|
|
pub fee_payer: Arc<Keypair>,
|
2022-05-27 22:05:34 -07:00
|
|
|
pub commitment: CommitmentConfig,
|
2022-08-04 08:01:00 -07:00
|
|
|
pub timeout: Option<Duration>,
|
2022-08-01 05:19:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Client {
|
2022-08-04 08:01:00 -07:00
|
|
|
pub fn new(
|
|
|
|
cluster: Cluster,
|
|
|
|
commitment: CommitmentConfig,
|
|
|
|
fee_payer: &Keypair,
|
|
|
|
timeout: Option<Duration>,
|
|
|
|
) -> Self {
|
2022-08-01 05:19:52 -07:00
|
|
|
Self {
|
|
|
|
cluster,
|
|
|
|
fee_payer: Arc::new(fee_payer.clone()),
|
|
|
|
commitment,
|
2022-08-04 08:01:00 -07:00
|
|
|
timeout,
|
2022-08-01 05:19:52 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn anchor_client(&self) -> anchor_client::Client {
|
|
|
|
anchor_client::Client::new_with_options(
|
|
|
|
self.cluster.clone(),
|
|
|
|
Rc::new((*self.fee_payer).clone()),
|
|
|
|
self.commitment,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-08-04 08:01:00 -07:00
|
|
|
pub fn rpc(&self) -> RpcClient {
|
|
|
|
let url = self.cluster.url().to_string();
|
|
|
|
if let Some(timeout) = self.timeout.as_ref() {
|
|
|
|
RpcClient::new_with_timeout_and_commitment(url, *timeout, self.commitment)
|
|
|
|
} else {
|
|
|
|
RpcClient::new_with_commitment(url, self.commitment)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn rpc_async(&self) -> RpcClientAsync {
|
|
|
|
let url = self.cluster.url().to_string();
|
|
|
|
if let Some(timeout) = self.timeout.as_ref() {
|
|
|
|
RpcClientAsync::new_with_timeout_and_commitment(url, *timeout, self.commitment)
|
|
|
|
} else {
|
|
|
|
RpcClientAsync::new_with_commitment(url, self.commitment)
|
|
|
|
}
|
2022-08-01 05:19:52 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// todo: might want to integrate geyser, websockets, or simple http polling for keeping data fresh
|
|
|
|
pub struct MangoClient {
|
|
|
|
pub client: Client,
|
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-08-01 05:19:52 -07:00
|
|
|
pub owner: Keypair,
|
2022-07-16 05:37:15 -07:00
|
|
|
pub mango_account_address: Pubkey,
|
|
|
|
|
|
|
|
pub context: MangoGroupContext,
|
2022-08-04 08:01:00 -07:00
|
|
|
|
|
|
|
// Since MangoClient currently provides a blocking interface, we'd prefer to use reqwest::blocking::Client
|
|
|
|
// but that doesn't work inside async contexts. Hence we use the async reqwest Client instead and use
|
|
|
|
// a manual runtime to bridge into async code from both sync and async contexts.
|
|
|
|
// That doesn't work perfectly, see MangoClient::invoke().
|
|
|
|
pub http_client: reqwest::Client,
|
|
|
|
runtime: Option<tokio::runtime::Runtime>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Drop for MangoClient {
|
|
|
|
fn drop(&mut self) {
|
|
|
|
self.runtime.take().expect("runtime").shutdown_background();
|
|
|
|
}
|
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-08-04 08:01:00 -07:00
|
|
|
pub fn find_accounts(
|
|
|
|
client: &Client,
|
|
|
|
group: Pubkey,
|
|
|
|
owner: &Keypair,
|
|
|
|
) -> anyhow::Result<Vec<(Pubkey, MangoAccountValue)>> {
|
|
|
|
let program = client.anchor_client().program(mango_v4::ID);
|
|
|
|
fetch_mango_accounts(&program, group, owner.pubkey()).map_err(Into::into)
|
|
|
|
}
|
|
|
|
|
2022-08-01 05:19:52 -07:00
|
|
|
pub fn find_or_create_account(
|
|
|
|
client: &Client,
|
2022-07-14 03:57:19 -07:00
|
|
|
group: Pubkey,
|
2022-08-04 08:01:00 -07:00
|
|
|
owner: &Keypair,
|
|
|
|
payer: &Keypair, // pays the SOL for the new account
|
2022-07-16 05:37:15 -07:00
|
|
|
mango_account_name: &str,
|
2022-08-01 05:19:52 -07:00
|
|
|
) -> anyhow::Result<Pubkey> {
|
|
|
|
let program = client.anchor_client().program(mango_v4::ID);
|
2022-07-13 05:04:20 -07:00
|
|
|
|
2022-05-29 03:25:12 -07:00
|
|
|
// Mango Account
|
2022-08-01 05:19:52 -07:00
|
|
|
let mut mango_account_tuples = fetch_mango_accounts(&program, group, owner.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
|
|
|
};
|
2022-08-04 08:01:00 -07:00
|
|
|
Self::create_account(client, group, owner, payer, account_num, mango_account_name)
|
2022-05-29 03:25:12 -07:00
|
|
|
.context("Failed to create account...")?;
|
|
|
|
}
|
2022-08-01 05:19:52 -07:00
|
|
|
let mango_account_tuples = fetch_mango_accounts(&program, group, owner.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-08-01 05:19:52 -07:00
|
|
|
Ok(mango_account_tuples[index].0)
|
|
|
|
}
|
|
|
|
|
2022-08-04 08:01:00 -07:00
|
|
|
pub fn create_account(
|
|
|
|
client: &Client,
|
|
|
|
group: Pubkey,
|
|
|
|
owner: &Keypair,
|
|
|
|
payer: &Keypair, // pays the SOL for the new account
|
|
|
|
account_num: u32,
|
|
|
|
mango_account_name: &str,
|
|
|
|
) -> anyhow::Result<(Pubkey, Signature)> {
|
|
|
|
let program = client.anchor_client().program(mango_v4::ID);
|
|
|
|
let account = Pubkey::find_program_address(
|
|
|
|
&[
|
|
|
|
group.as_ref(),
|
|
|
|
b"MangoAccount".as_ref(),
|
|
|
|
owner.pubkey().as_ref(),
|
|
|
|
&account_num.to_le_bytes(),
|
|
|
|
],
|
|
|
|
&mango_v4::id(),
|
|
|
|
)
|
|
|
|
.0;
|
|
|
|
let txsig = program
|
|
|
|
.request()
|
|
|
|
.instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
|
|
|
&mango_v4::accounts::AccountCreate {
|
|
|
|
group,
|
|
|
|
owner: owner.pubkey(),
|
|
|
|
account,
|
|
|
|
payer: payer.pubkey(),
|
|
|
|
system_program: System::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
),
|
|
|
|
data: anchor_lang::InstructionData::data(&mango_v4::instruction::AccountCreate {
|
|
|
|
account_num,
|
|
|
|
name: mango_account_name.to_owned(),
|
2022-08-07 05:16:06 -07:00
|
|
|
token_count: 8,
|
|
|
|
serum3_count: 8,
|
|
|
|
perp_count: 8,
|
|
|
|
perp_oo_count: 8,
|
2022-08-04 08:01:00 -07:00
|
|
|
}),
|
|
|
|
})
|
|
|
|
.signer(owner)
|
|
|
|
.signer(payer)
|
|
|
|
.send()
|
|
|
|
.map_err(prettify_client_error)?;
|
|
|
|
|
|
|
|
Ok((account, txsig))
|
|
|
|
}
|
|
|
|
|
2022-08-01 05:19:52 -07:00
|
|
|
/// Conveniently creates a RPC based client
|
|
|
|
pub fn new_for_existing_account(
|
|
|
|
client: Client,
|
|
|
|
account: Pubkey,
|
|
|
|
owner: Keypair,
|
|
|
|
) -> anyhow::Result<Self> {
|
2022-08-04 08:01:00 -07:00
|
|
|
let rpc = client.rpc();
|
2022-08-01 05:19:52 -07:00
|
|
|
let account_fetcher = Arc::new(CachedAccountFetcher::new(RpcAccountFetcher { rpc }));
|
|
|
|
let mango_account = account_fetcher_fetch_mango_account(&*account_fetcher, account)?;
|
|
|
|
let group = mango_account.fixed.group;
|
|
|
|
if mango_account.fixed.owner != owner.pubkey() {
|
|
|
|
anyhow::bail!(
|
|
|
|
"bad owner for account: expected {} got {}",
|
|
|
|
mango_account.fixed.owner,
|
|
|
|
owner.pubkey()
|
|
|
|
);
|
|
|
|
}
|
2022-05-27 22:05:34 -07:00
|
|
|
|
2022-08-01 05:19:52 -07:00
|
|
|
let group_context =
|
|
|
|
MangoGroupContext::new_from_rpc(group, client.cluster.clone(), client.commitment)?;
|
|
|
|
|
|
|
|
Self::new_detail(client, account, owner, group_context, account_fetcher)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Allows control of AccountFetcher and externally created MangoGroupContext
|
|
|
|
pub fn new_detail(
|
|
|
|
client: Client,
|
|
|
|
account: Pubkey,
|
|
|
|
owner: Keypair,
|
|
|
|
// future: maybe pass Arc<MangoGroupContext>, so it can be extenally updated?
|
|
|
|
group_context: MangoGroupContext,
|
|
|
|
account_fetcher: Arc<dyn AccountFetcher>,
|
|
|
|
) -> anyhow::Result<Self> {
|
2022-05-27 22:05:34 -07:00
|
|
|
Ok(Self {
|
2022-08-01 05:19:52 -07:00
|
|
|
client,
|
2022-07-16 05:37:15 -07:00
|
|
|
account_fetcher,
|
2022-08-01 05:19:52 -07:00
|
|
|
owner,
|
|
|
|
mango_account_address: account,
|
2022-07-16 05:37:15 -07:00
|
|
|
context: group_context,
|
2022-08-04 08:01:00 -07:00
|
|
|
http_client: reqwest::Client::new(),
|
|
|
|
runtime: Some(
|
|
|
|
tokio::runtime::Builder::new_current_thread()
|
|
|
|
.thread_name("mango-client")
|
|
|
|
.enable_io()
|
|
|
|
.enable_time()
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
),
|
2022-05-27 22:05:34 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-08-01 05:19:52 -07:00
|
|
|
pub fn anchor_client(&self) -> anchor_client::Client {
|
|
|
|
self.client.anchor_client()
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn program(&self) -> Program {
|
2022-08-01 05:19:52 -07:00
|
|
|
self.anchor_client().program(mango_v4::ID)
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
2022-08-01 05:19:52 -07:00
|
|
|
pub fn owner(&self) -> Pubkey {
|
|
|
|
self.owner.pubkey()
|
2022-05-27 22:05:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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-08-04 08:01:00 -07:00
|
|
|
affected_tokens: Vec<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,
|
2022-08-04 08:01:00 -07:00
|
|
|
affected_tokens,
|
2022-07-16 05:37:15 -07:00
|
|
|
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
|
|
|
|
|
|
|
Ok(banks
|
|
|
|
.iter()
|
|
|
|
.map(|(pubkey, is_writable)| AccountMeta {
|
|
|
|
pubkey: *pubkey,
|
|
|
|
is_writable: *is_writable,
|
|
|
|
is_signer: false,
|
|
|
|
})
|
2022-08-04 08:01:00 -07:00
|
|
|
.chain(oracles.into_iter().map(to_readonly_account_meta))
|
|
|
|
.chain(perp_markets.map(to_readonly_account_meta))
|
|
|
|
.chain(serum_oos.map(to_readonly_account_meta))
|
2022-05-27 22:05:34 -07:00
|
|
|
.collect())
|
|
|
|
}
|
|
|
|
|
2022-08-04 08:01:00 -07:00
|
|
|
pub fn token_deposit(&self, mint: Pubkey, amount: u64) -> anyhow::Result<Signature> {
|
|
|
|
let token = self.context.token_by_mint(&mint)?;
|
|
|
|
let token_index = token.token_index;
|
|
|
|
let mint_info = token.mint_info;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
let health_check_metas =
|
2022-08-04 08:01:00 -07:00
|
|
|
self.derive_health_check_remaining_account_metas(vec![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(
|
2022-08-01 05:19:52 -07:00
|
|
|
&self.owner(),
|
2022-05-27 22:05:34 -07:00
|
|
|
&mint_info.mint,
|
|
|
|
),
|
2022-08-01 05:19:52 -07:00
|
|
|
token_authority: self.owner(),
|
2022-05-27 22:05:34 -07:00
|
|
|
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,
|
|
|
|
}),
|
|
|
|
})
|
2022-08-01 05:19:52 -07:00
|
|
|
.signer(&self.owner)
|
2022-05-27 22:05:34 -07:00
|
|
|
.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,
|
2022-08-01 05:19:52 -07:00
|
|
|
owner: self.owner(),
|
|
|
|
payer: self.owner(),
|
2022-05-27 22:05:34 -07:00
|
|
|
system_program: System::id(),
|
|
|
|
rent: sysvar::rent::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
),
|
|
|
|
data: anchor_lang::InstructionData::data(
|
|
|
|
&mango_v4::instruction::Serum3CreateOpenOrders {},
|
|
|
|
),
|
|
|
|
})
|
2022-08-01 05:19:52 -07:00
|
|
|
.signer(&self.owner)
|
2022-05-27 22:05:34 -07:00
|
|
|
.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
|
|
|
|
2022-08-04 08:01:00 -07:00
|
|
|
let health_check_metas = self.derive_health_check_remaining_account_metas(vec![], false)?;
|
2022-05-27 22:05:34 -07:00
|
|
|
|
|
|
|
// 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-08-01 05:19:52 -07:00
|
|
|
owner: self.owner(),
|
2022-05-27 22:05:34 -07:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
})
|
2022-08-01 05:19:52 -07:00
|
|
|
.signer(&self.owner)
|
2022-05-27 22:05:34 -07:00
|
|
|
.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-08-01 05:19:52 -07:00
|
|
|
owner: self.owner(),
|
2022-05-27 22:05:34 -07:00
|
|
|
token_program: Token::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
),
|
|
|
|
data: anchor_lang::InstructionData::data(
|
|
|
|
&mango_v4::instruction::Serum3SettleFunds {},
|
|
|
|
),
|
|
|
|
})
|
2022-08-01 05:19:52 -07:00
|
|
|
.signer(&self.owner)
|
2022-05-27 22:05:34 -07:00
|
|
|
.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-08-01 05:19:52 -07:00
|
|
|
owner: self.owner(),
|
2022-05-27 22:05:34 -07:00
|
|
|
},
|
|
|
|
None,
|
|
|
|
)
|
|
|
|
},
|
|
|
|
data: anchor_lang::InstructionData::data(
|
|
|
|
&mango_v4::instruction::Serum3CancelOrder { side, order_id },
|
|
|
|
),
|
|
|
|
})
|
2022-08-01 05:19:52 -07:00
|
|
|
.signer(&self.owner)
|
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-08-01 05:19:52 -07:00
|
|
|
liqor_owner: self.owner(),
|
2022-06-18 07:31:28 -07:00
|
|
|
},
|
|
|
|
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,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
})
|
2022-08-01 05:19:52 -07:00
|
|
|
.signer(&self.owner)
|
2022-06-18 07:31:28 -07:00
|
|
|
.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()
|
2022-08-04 08:01:00 -07:00
|
|
|
.map(|bank_pubkey| to_writable_account_meta(*bank_pubkey))
|
2022-07-13 05:04:20 -07:00
|
|
|
.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-08-01 05:19:52 -07:00
|
|
|
liqor_owner: self.owner(),
|
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,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
})
|
2022-08-01 05:19:52 -07:00
|
|
|
.signer(&self.owner)
|
2022-07-13 05:04:20 -07:00
|
|
|
.send()
|
2022-07-15 01:00:01 -07:00
|
|
|
.map_err(prettify_client_error)
|
2022-07-13 05:04:20 -07:00
|
|
|
}
|
2022-08-04 08:01:00 -07:00
|
|
|
|
|
|
|
pub fn jupiter_swap(
|
|
|
|
&self,
|
|
|
|
input_mint: Pubkey,
|
|
|
|
output_mint: Pubkey,
|
|
|
|
source_amount: u64,
|
|
|
|
slippage: f64,
|
|
|
|
) -> anyhow::Result<Signature> {
|
|
|
|
self.invoke(self.jupiter_swap_async(input_mint, output_mint, source_amount, slippage))
|
|
|
|
}
|
|
|
|
|
2022-08-05 06:24:23 -07:00
|
|
|
pub fn jupiter_route(
|
2022-08-04 08:01:00 -07:00
|
|
|
&self,
|
|
|
|
input_mint: Pubkey,
|
|
|
|
output_mint: Pubkey,
|
|
|
|
source_amount: u64,
|
|
|
|
slippage: f64,
|
2022-08-05 06:24:23 -07:00
|
|
|
) -> anyhow::Result<jupiter::QueryRoute> {
|
|
|
|
self.invoke(self.jupiter_route_async(input_mint, output_mint, source_amount, slippage))
|
|
|
|
}
|
2022-08-04 08:01:00 -07:00
|
|
|
|
2022-08-05 06:24:23 -07:00
|
|
|
pub async fn jupiter_route_async(
|
|
|
|
&self,
|
|
|
|
input_mint: Pubkey,
|
|
|
|
output_mint: Pubkey,
|
|
|
|
source_amount: u64,
|
|
|
|
slippage: f64,
|
|
|
|
) -> anyhow::Result<jupiter::QueryRoute> {
|
2022-08-04 08:01:00 -07:00
|
|
|
let quote = self
|
|
|
|
.http_client
|
|
|
|
.get("https://quote-api.jup.ag/v1/quote")
|
|
|
|
.query(&[
|
|
|
|
("inputMint", input_mint.to_string()),
|
|
|
|
("outputMint", output_mint.to_string()),
|
|
|
|
("amount", format!("{}", source_amount)),
|
|
|
|
("onlyDirectRoutes", "true".into()),
|
|
|
|
("filterTopNResult", "10".into()),
|
|
|
|
("slippage", format!("{}", slippage)),
|
|
|
|
])
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.context("quote request to jupiter")?
|
|
|
|
.json::<jupiter::QueryResult>()
|
|
|
|
.await
|
|
|
|
.context("receiving json response from jupiter quote request")?;
|
|
|
|
|
|
|
|
// Find the top route that doesn't involve Raydium (that has too many accounts)
|
|
|
|
let route = quote
|
|
|
|
.data
|
|
|
|
.iter()
|
|
|
|
.find(|route| {
|
|
|
|
!route
|
|
|
|
.market_infos
|
|
|
|
.iter()
|
|
|
|
.any(|mi| mi.label.contains("Raydium"))
|
|
|
|
})
|
|
|
|
.ok_or_else(|| {
|
|
|
|
anyhow::anyhow!(
|
|
|
|
"no route for swap. found {} routes, but none were usable",
|
|
|
|
quote.data.len()
|
|
|
|
)
|
|
|
|
})?;
|
|
|
|
|
2022-08-05 06:24:23 -07:00
|
|
|
Ok(route.clone())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn jupiter_swap_async(
|
|
|
|
&self,
|
|
|
|
input_mint: Pubkey,
|
|
|
|
output_mint: Pubkey,
|
|
|
|
source_amount: u64,
|
|
|
|
slippage: f64,
|
|
|
|
) -> anyhow::Result<Signature> {
|
|
|
|
let source_token = self.context.token_by_mint(&input_mint)?;
|
|
|
|
let target_token = self.context.token_by_mint(&output_mint)?;
|
|
|
|
let route = self
|
|
|
|
.jupiter_route_async(input_mint, output_mint, source_amount, slippage)
|
|
|
|
.await?;
|
|
|
|
|
2022-08-04 08:01:00 -07:00
|
|
|
let swap = self
|
|
|
|
.http_client
|
|
|
|
.post("https://quote-api.jup.ag/v1/swap")
|
|
|
|
.json(&jupiter::SwapRequest {
|
|
|
|
route: route.clone(),
|
|
|
|
user_public_key: self.owner.pubkey().to_string(),
|
|
|
|
wrap_unwrap_sol: false,
|
|
|
|
})
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.context("swap transaction request to jupiter")?
|
|
|
|
.json::<jupiter::SwapResponse>()
|
|
|
|
.await
|
|
|
|
.context("receiving json response from jupiter swap transaction request")?;
|
|
|
|
|
|
|
|
if swap.setup_transaction.is_some() || swap.cleanup_transaction.is_some() {
|
|
|
|
anyhow::bail!(
|
|
|
|
"chosen jupiter route requires setup or cleanup transactions, can't execute"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: deal with versioned transaction!
|
|
|
|
let jup_tx = bincode::options()
|
|
|
|
.with_fixint_encoding()
|
|
|
|
.reject_trailing_bytes()
|
|
|
|
.deserialize::<solana_sdk::transaction::Transaction>(
|
|
|
|
&base64::decode(&swap.swap_transaction)
|
|
|
|
.context("base64 decoding jupiter transaction")?,
|
|
|
|
)
|
|
|
|
.context("parsing jupiter transaction")?;
|
|
|
|
let jup_ixs = deserialize_instructions(&jup_tx.message)
|
|
|
|
.into_iter()
|
|
|
|
// TODO: possibly creating associated token accounts if they don't exist yet is good?!
|
|
|
|
// we could squeeze the FlashLoan instructions in the middle:
|
|
|
|
// - beginning AToken...
|
|
|
|
// - FlashLoanBegin
|
|
|
|
// - other JUP ix
|
|
|
|
// - FlashLoanEnd
|
|
|
|
// - ending AToken
|
|
|
|
.filter(|ix| {
|
|
|
|
ix.program_id
|
|
|
|
!= Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap()
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
let bank_ams = [
|
|
|
|
source_token.mint_info.first_bank(),
|
|
|
|
target_token.mint_info.first_bank(),
|
|
|
|
]
|
|
|
|
.into_iter()
|
|
|
|
.map(to_writable_account_meta)
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
let vault_ams = [
|
|
|
|
source_token.mint_info.first_vault(),
|
|
|
|
target_token.mint_info.first_vault(),
|
|
|
|
]
|
|
|
|
.into_iter()
|
|
|
|
.map(to_writable_account_meta)
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
let token_ams = [source_token.mint_info.mint, target_token.mint_info.mint]
|
|
|
|
.into_iter()
|
|
|
|
.map(|mint| {
|
|
|
|
to_writable_account_meta(
|
|
|
|
anchor_spl::associated_token::get_associated_token_address(
|
|
|
|
&self.owner(),
|
|
|
|
&mint,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
let loan_amounts = vec![source_amount, 0u64];
|
|
|
|
|
|
|
|
// This relies on the fact that health account banks will be identical to the first_bank above!
|
|
|
|
let health_ams = self
|
|
|
|
.derive_health_check_remaining_account_metas(
|
|
|
|
vec![source_token.token_index, target_token.token_index],
|
|
|
|
true,
|
|
|
|
)
|
|
|
|
.context("building health accounts")?;
|
|
|
|
|
|
|
|
let program = self.program();
|
|
|
|
let mut builder = program.request().instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: {
|
|
|
|
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
|
|
|
&mango_v4::accounts::FlashLoanBegin {
|
|
|
|
group: self.group(),
|
|
|
|
token_program: Token::id(),
|
|
|
|
instructions: solana_sdk::sysvar::instructions::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
);
|
|
|
|
ams.extend(bank_ams);
|
|
|
|
ams.extend(vault_ams.clone());
|
|
|
|
ams.extend(token_ams.clone());
|
|
|
|
ams
|
|
|
|
},
|
|
|
|
data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanBegin {
|
|
|
|
loan_amounts,
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
for ix in jup_ixs {
|
|
|
|
builder = builder.instruction(ix);
|
|
|
|
}
|
|
|
|
builder = builder.instruction(Instruction {
|
|
|
|
program_id: mango_v4::id(),
|
|
|
|
accounts: {
|
|
|
|
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
|
|
|
&mango_v4::accounts::FlashLoanEnd {
|
|
|
|
account: self.mango_account_address,
|
|
|
|
owner: self.owner(),
|
|
|
|
token_program: Token::id(),
|
|
|
|
},
|
|
|
|
None,
|
|
|
|
);
|
|
|
|
ams.extend(health_ams);
|
|
|
|
ams.extend(vault_ams);
|
|
|
|
ams.extend(token_ams);
|
|
|
|
ams
|
|
|
|
},
|
|
|
|
data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanEnd {}),
|
|
|
|
});
|
|
|
|
|
|
|
|
let rpc = self.client.rpc_async();
|
|
|
|
builder
|
|
|
|
.signer(&self.owner)
|
|
|
|
.send_rpc_async(&rpc)
|
|
|
|
.await
|
|
|
|
.map_err(prettify_client_error)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn invoke<T, F: std::future::Future<Output = T>>(&self, f: F) -> T {
|
|
|
|
// `block_on()` panics if called within an asynchronous execution context. Whereas
|
|
|
|
// `block_in_place()` only panics if called from a current_thread runtime, which is the
|
|
|
|
// lesser evil.
|
|
|
|
tokio::task::block_in_place(move || self.runtime.as_ref().expect("runtime").block_on(f))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn deserialize_instructions(message: &solana_sdk::message::Message) -> Vec<Instruction> {
|
|
|
|
message
|
|
|
|
.instructions
|
|
|
|
.iter()
|
|
|
|
.map(|ci| solana_sdk::instruction::Instruction {
|
|
|
|
program_id: *ci.program_id(&message.account_keys),
|
|
|
|
accounts: ci
|
|
|
|
.accounts
|
|
|
|
.iter()
|
|
|
|
.map(|&index| AccountMeta {
|
|
|
|
pubkey: message.account_keys[index as usize],
|
|
|
|
is_signer: message.is_signer(index.into()),
|
|
|
|
is_writable: message.is_writable(index.into()),
|
|
|
|
})
|
|
|
|
.collect(),
|
|
|
|
data: ci.data.clone(),
|
|
|
|
})
|
|
|
|
.collect()
|
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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-01 08:53:13 -07:00
|
|
|
|
|
|
|
pub fn pubkey_from_cli(pubkey: &str) -> Pubkey {
|
|
|
|
match Pubkey::from_str(pubkey) {
|
|
|
|
Ok(p) => p,
|
|
|
|
Err(_) => keypair_from_cli(pubkey).pubkey(),
|
|
|
|
}
|
|
|
|
}
|
2022-08-04 08:01:00 -07:00
|
|
|
|
|
|
|
fn to_readonly_account_meta(pubkey: Pubkey) -> AccountMeta {
|
|
|
|
AccountMeta {
|
|
|
|
pubkey,
|
|
|
|
is_writable: false,
|
|
|
|
is_signer: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn to_writable_account_meta(pubkey: Pubkey) -> AccountMeta {
|
|
|
|
AccountMeta {
|
|
|
|
pubkey,
|
|
|
|
is_writable: true,
|
|
|
|
is_signer: false,
|
|
|
|
}
|
|
|
|
}
|