Rust client: auto close spot account if needed to place a new order (#877)

rust client: 
- add serum3 place order to command
- add serum3 create open orders command
- add serum3 close open orders command
- auto create serum3 open orders if needed when placing a new order
- auto close serum3 slot if needed when placing a new order & also close unused token if needed to place a serum3 order
This commit is contained in:
Serge Farny 2024-02-16 14:01:18 +01:00 committed by GitHub
parent 34d86ef0f4
commit 5d29eb2f0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 435 additions and 56 deletions

View File

@ -1,5 +1,6 @@
use clap::clap_derive::ArgEnum;
use clap::{Args, Parser, Subcommand};
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::state::{PlaceOrderType, SelfTradeBehavior, Side};
use mango_v4_client::{
keypair_from_cli, pubkey_from_cli, Client, MangoClient, TransactionBuilderConfig,
@ -126,6 +127,63 @@ struct PerpPlaceOrder {
rpc: Rpc,
}
#[derive(Args, Debug, Clone)]
struct Serum3CreateOpenOrders {
#[clap(long)]
account: String,
/// also pays for everything
#[clap(short, long)]
owner: String,
#[clap(long)]
market_name: String,
#[clap(flatten)]
rpc: Rpc,
}
#[derive(Args, Debug, Clone)]
struct Serum3CloseOpenOrders {
#[clap(long)]
account: String,
/// also pays for everything
#[clap(short, long)]
owner: String,
#[clap(long)]
market_name: String,
#[clap(flatten)]
rpc: Rpc,
}
#[derive(Args, Debug, Clone)]
struct Serum3PlaceOrder {
#[clap(long)]
account: String,
/// also pays for everything
#[clap(short, long)]
owner: String,
#[clap(long)]
market_name: String,
#[clap(long, value_enum)]
side: CliSide,
#[clap(short, long)]
price: f64,
#[clap(long)]
quantity: f64,
#[clap(flatten)]
rpc: Rpc,
}
#[derive(Subcommand, Debug, Clone)]
enum Command {
CreateAccount(CreateAccount),
@ -167,6 +225,9 @@ enum Command {
output: String,
},
PerpPlaceOrder(PerpPlaceOrder),
Serum3CloseOpenOrders(Serum3CloseOpenOrders),
Serum3CreateOpenOrders(Serum3CreateOpenOrders),
Serum3PlaceOrder(Serum3PlaceOrder),
}
impl Rpc {
@ -326,6 +387,65 @@ async fn main() -> Result<(), anyhow::Error> {
.await?;
println!("{}", txsig);
}
Command::Serum3CreateOpenOrders(cmd) => {
let client = cmd.rpc.client(Some(&cmd.owner))?;
let account = pubkey_from_cli(&cmd.account);
let owner = Arc::new(keypair_from_cli(&cmd.owner));
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let txsig = client.serum3_create_open_orders(&cmd.market_name).await?;
println!("{}", txsig);
}
Command::Serum3CloseOpenOrders(cmd) => {
let client = cmd.rpc.client(Some(&cmd.owner))?;
let account = pubkey_from_cli(&cmd.account);
let owner = Arc::new(keypair_from_cli(&cmd.owner));
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let txsig = client.serum3_close_open_orders(&cmd.market_name).await?;
println!("{}", txsig);
}
Command::Serum3PlaceOrder(cmd) => {
let client = cmd.rpc.client(Some(&cmd.owner))?;
let account = pubkey_from_cli(&cmd.account);
let owner = Arc::new(keypair_from_cli(&cmd.owner));
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let market_index = client.context.serum3_market_index(&cmd.market_name);
let market = client.context.serum3(market_index);
let base_token = client.context.token(market.base_token_index);
let quote_token = client.context.token(market.quote_token_index);
fn native(x: f64, b: u32) -> u64 {
(x * (10_i64.pow(b)) as f64) as u64
}
// coin_lot_size = base lot size ?
// cf priceNumberToLots
let price_lots = native(cmd.price, quote_token.decimals as u32) * market.coin_lot_size
/ (native(1.0, base_token.decimals as u32) * market.pc_lot_size);
// cf baseSizeNumberToLots
let max_base_lots =
native(cmd.quantity, base_token.decimals as u32) / market.coin_lot_size;
let txsig = client
.serum3_place_order(
&cmd.market_name,
match cmd.side {
CliSide::Bid => Serum3Side::Bid,
CliSide::Ask => Serum3Side::Ask,
},
price_lots,
max_base_lots as u64,
((price_lots * max_base_lots) as f64 * 1.01) as u64,
Serum3SelfTradeBehavior::AbortTransaction,
Serum3OrderType::Limit,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
10,
)
.await?;
println!("{}", txsig);
}
};
Ok(())

View File

@ -1,3 +1,4 @@
use anchor_client::ClientError::AnchorError;
use std::ops::Deref;
use std::str::FromStr;
use std::sync::Arc;
@ -126,6 +127,7 @@ impl ClientBuilder {
}
}
}
pub struct Client {
config: ClientConfig,
rpc_async: RpcClientAsync,
@ -577,40 +579,40 @@ impl MangoClient {
let ixs = PreparedInstructions::from_vec(
vec![
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
&self.owner(),
&self.owner(),
&mint,
&Token::id(),
),
Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenWithdraw {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
bank: token.first_bank(),
vault: token.first_vault(),
oracle: token.oracle,
token_account: get_associated_token_address(
&self.owner(),
&token.mint,
),
token_program: Token::id(),
},
None,
);
ams.extend(health_check_metas.into_iter());
ams
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
&self.owner(),
&self.owner(),
&mint,
&Token::id(),
),
Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenWithdraw {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
bank: token.first_bank(),
vault: token.first_vault(),
oracle: token.oracle,
token_account: get_associated_token_address(
&self.owner(),
&token.mint,
),
token_program: Token::id(),
},
None,
);
ams.extend(health_check_metas.into_iter());
ams
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenWithdraw {
amount,
allow_borrow,
}),
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenWithdraw {
amount,
allow_borrow,
}),
},
],
],
self.instruction_cu(health_cu),
);
Ok(ixs)
@ -658,6 +660,45 @@ impl MangoClient {
// Serum3
//
pub fn serum3_close_open_orders_instruction(
&self,
market_index: Serum3MarketIndex,
) -> PreparedInstructions {
let account_pubkey = self.mango_account_address;
let s3 = self.context.serum3(market_index);
let open_orders = self.serum3_create_open_orders_address(market_index);
PreparedInstructions::from_single(
Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::Serum3CloseOpenOrders {
group: self.group(),
account: account_pubkey,
serum_market: s3.address,
serum_program: s3.serum_program,
serum_market_external: s3.serum_market_external,
open_orders,
owner: self.owner(),
sol_destination: self.owner(),
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::Serum3CloseOpenOrders {},
),
},
self.context.compute_estimates.cu_per_mango_instruction,
)
}
pub async fn serum3_close_open_orders(&self, name: &str) -> anyhow::Result<Signature> {
let market_index = self.context.serum3_market_index(name);
let ix = self.serum3_close_open_orders_instruction(market_index);
self.send_and_confirm_owner_tx(ix.to_instructions()).await
}
pub fn serum3_create_open_orders_instruction(
&self,
market_index: Serum3MarketIndex,
@ -665,15 +706,7 @@ impl MangoClient {
let account_pubkey = self.mango_account_address;
let s3 = self.context.serum3(market_index);
let open_orders = Pubkey::find_program_address(
&[
b"Serum3OO".as_ref(),
account_pubkey.as_ref(),
s3.address.as_ref(),
],
&mango_v4::ID,
)
.0;
let open_orders = self.serum3_create_open_orders_address(market_index);
Instruction {
program_id: mango_v4::id(),
@ -698,6 +731,23 @@ impl MangoClient {
}
}
fn serum3_create_open_orders_address(&self, market_index: Serum3MarketIndex) -> Pubkey {
let account_pubkey = self.mango_account_address;
let s3 = self.context.serum3(market_index);
let open_orders = Pubkey::find_program_address(
&[
b"Serum3OO".as_ref(),
account_pubkey.as_ref(),
s3.address.as_ref(),
],
&mango_v4::ID,
)
.0;
open_orders
}
pub async fn serum3_create_open_orders(&self, name: &str) -> anyhow::Result<Signature> {
let market_index = self.context.serum3_market_index(name);
let ix = self.serum3_create_open_orders_instruction(market_index);
@ -721,20 +771,22 @@ impl MangoClient {
let s3 = self.context.serum3(market_index);
let base = self.context.serum3_base_token(market_index);
let quote = self.context.serum3_quote_token(market_index);
let open_orders = account
.serum3_orders(market_index)
.expect("oo is created")
.open_orders;
let (payer_token, receiver_token) = match side {
Serum3Side::Bid => (&quote, &base),
Serum3Side::Ask => (&base, &quote),
};
let open_orders = account.serum3_orders(market_index).map(|x| x.open_orders)?;
let (health_check_metas, health_cu) = self
.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])
.derive_health_check_remaining_account_metas(
&account,
vec![],
vec![receiver_token.token_index],
vec![],
)
.await?;
let payer_token = match side {
Serum3Side::Bid => &quote,
Serum3Side::Ask => &base,
};
let ixs = PreparedInstructions::from_single(
Instruction {
program_id: mango_v4::id(),
@ -766,7 +818,7 @@ impl MangoClient {
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::Serum3PlaceOrder {
&mango_v4::instruction::Serum3PlaceOrderV2 {
side,
limit_price,
max_base_qty,
@ -785,6 +837,205 @@ impl MangoClient {
Ok(ixs)
}
#[allow(clippy::too_many_arguments)]
pub async fn serum3_create_or_replace_account_instruction(
&self,
mut account: &mut MangoAccountValue,
market_index: Serum3MarketIndex,
side: Serum3Side,
) -> anyhow::Result<PreparedInstructions> {
let mut ixs = PreparedInstructions::new();
let base = self.context.serum3_base_token(market_index);
let quote = self.context.serum3_quote_token(market_index);
let (payer_token, receiver_token) = match side {
Serum3Side::Bid => (&quote, &base),
Serum3Side::Ask => (&base, &quote),
};
let open_orders_opt = account
.serum3_orders(market_index)
.map(|x| x.open_orders)
.ok();
let mut missing_tokens = false;
let token_replace_ixs = self
.find_existing_or_try_to_replace_token_positions(
&mut account,
&[payer_token.token_index, receiver_token.token_index],
)
.await;
match token_replace_ixs {
Ok(res) => {
ixs.append(res);
}
Err(_) => missing_tokens = true,
}
if open_orders_opt.is_none() {
let has_available_slot = account.all_serum3_orders().any(|p| !p.is_active());
let should_close_one_open_orders_account = !has_available_slot || missing_tokens;
if should_close_one_open_orders_account {
ixs.append(
self.deactivate_first_active_unused_serum3_orders(&mut account)
.await?,
);
}
// in case of missing token slots
// try again to create, as maybe deactivating the market slot resulted in some token being now unused
// but this time, in case of error, propagate to caller
if missing_tokens {
ixs.append(
self.find_existing_or_try_to_replace_token_positions(
&mut account,
&[payer_token.token_index, receiver_token.token_index],
)
.await?,
);
}
ixs.push(
self.serum3_create_open_orders_instruction(market_index),
self.context.compute_estimates.cu_per_mango_instruction,
);
let created_open_orders = self.serum3_create_open_orders_address(market_index);
account.create_serum3_orders(market_index)?.open_orders = created_open_orders;
}
Ok(ixs)
}
async fn deactivate_first_active_unused_serum3_orders(
&self,
account: &mut MangoAccountValue,
) -> anyhow::Result<PreparedInstructions> {
let mut serum3_closable_order_market_index = None;
for p in account.all_serum3_orders() {
let open_orders_acc = self
.account_fetcher
.fetch_raw_account(&p.open_orders)
.await?;
let open_orders_bytes = open_orders_acc.data();
let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes(
&open_orders_bytes[5..5 + std::mem::size_of::<serum_dex::state::OpenOrders>()],
);
let is_closable = open_orders_data.free_slot_bits == u128::MAX
&& open_orders_data.native_coin_total == 0
&& open_orders_data.native_pc_total == 0;
if is_closable {
serum3_closable_order_market_index = Some(p.market_index);
break;
}
}
let first_closable_slot =
serum3_closable_order_market_index.expect("couldn't find any serum3 slot available");
let ixs = self.serum3_close_open_orders_instruction(first_closable_slot);
let first_closable_market = account.serum3_orders(first_closable_slot)?;
let (tk1, tk2) = (
first_closable_market.base_token_index,
first_closable_market.quote_token_index,
);
account.token_position_mut(tk1)?.0.decrement_in_use();
account.token_position_mut(tk2)?.0.decrement_in_use();
account.deactivate_serum3_orders(first_closable_slot)?;
Ok(ixs)
}
async fn find_existing_or_try_to_replace_token_positions(
&self,
account: &mut MangoAccountValue,
token_indexes: &[TokenIndex],
) -> anyhow::Result<PreparedInstructions> {
let mut ixs = PreparedInstructions::new();
for token_index in token_indexes {
let result = self
.find_existing_or_try_to_replace_token_position(account, *token_index)
.await?;
if let Some(ix) = result {
ixs.append(ix);
}
}
Ok(ixs)
}
async fn find_existing_or_try_to_replace_token_position(
&self,
account: &mut MangoAccountValue,
token_index: TokenIndex,
) -> anyhow::Result<Option<PreparedInstructions>> {
let token_position_missing = account
.ensure_token_position(token_index)
.is_anchor_error_with_code(MangoError::NoFreeTokenPositionIndex.error_code());
if !token_position_missing {
return Ok(None);
}
let ixs = self.deactivate_first_active_unused_token(account).await?;
account.ensure_token_position(token_index)?;
Ok(Some(ixs))
}
async fn deactivate_first_active_unused_token(
&self,
account: &mut MangoAccountValue,
) -> anyhow::Result<PreparedInstructions> {
let closable_tokens = account
.all_token_positions()
.enumerate()
.filter(|(_, p)| p.is_active() && !p.is_in_use());
let mut closable_token_position_raw_index_opt = None;
let mut closable_token_bank_opt = None;
for (closable_token_position_raw_index, closable_token_position) in closable_tokens {
let bank = self.first_bank(closable_token_position.token_index).await?;
let native_balance = closable_token_position.native(&bank);
if native_balance < I80F48::ZERO {
continue;
}
if native_balance > I80F48::ONE {
continue;
}
closable_token_position_raw_index_opt = Some(closable_token_position_raw_index);
closable_token_bank_opt = Some(bank);
break;
}
if closable_token_bank_opt.is_none() {
return Err(AnchorError(MangoError::NoFreeTokenPositionIndex.into()).into());
}
let withdraw_ixs = self
.token_withdraw_instructions(
&account,
closable_token_bank_opt.unwrap().mint,
u64::MAX,
false,
)
.await?;
account.deactivate_token_position(closable_token_position_raw_index_opt.unwrap());
return Ok(withdraw_ixs);
}
#[allow(clippy::too_many_arguments)]
pub async fn serum3_place_order(
&self,
@ -798,9 +1049,12 @@ impl MangoClient {
client_order_id: u64,
limit: u16,
) -> anyhow::Result<Signature> {
let account = self.mango_account().await?;
let mut account = self.mango_account().await?.clone();
let market_index = self.context.serum3_market_index(name);
let ixs = self
let create_or_replace_ixs = self
.serum3_create_or_replace_account_instruction(&mut account, market_index, side)
.await?;
let place_order_ixs = self
.serum3_place_order_instruction(
&account,
market_index,
@ -814,6 +1068,10 @@ impl MangoClient {
limit,
)
.await?;
let mut ixs = PreparedInstructions::new();
ixs.append(create_or_replace_ixs);
ixs.append(place_order_ixs);
self.send_and_confirm_owner_tx(ixs.to_instructions()).await
}
@ -1960,7 +2218,7 @@ impl MangoClient {
#[derive(Debug, thiserror::Error)]
pub enum MangoClientError {
#[error("Transaction simulation error. Error: {err:?}, Logs: {}",
.logs.iter().join("; ")
.logs.iter().join("; ")
)]
SendTransactionPreflightFailure {
err: Option<TransactionError>,
@ -1999,6 +2257,7 @@ pub enum FallbackOracleConfig {
/// Every possible fallback oracle (may cause serious issues with the 64 accounts-per-tx limit)
All,
}
impl Default for FallbackOracleConfig {
fn default() -> Self {
FallbackOracleConfig::Dynamic