rust client: Perp - Auto close account when needed (#875)

* perp: auto close perp market account when needing to open a new one with no slot available

* rust_client: do not send health accounts when deactivating a perp position (not needed on program side)

* rust_client: add perp place order command
This commit is contained in:
Serge Farny 2024-02-09 11:19:52 +01:00 committed by GitHub
parent b5d49381ed
commit 08a5ee8f53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 228 additions and 43 deletions

View File

@ -1,10 +1,13 @@
use clap::clap_derive::ArgEnum;
use clap::{Args, Parser, Subcommand};
use mango_v4::state::{PlaceOrderType, SelfTradeBehavior, Side};
use mango_v4_client::{
keypair_from_cli, pubkey_from_cli, Client, MangoClient, TransactionBuilderConfig,
};
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
mod save_snapshot;
mod test_oracles;
@ -88,6 +91,41 @@ struct JupiterSwap {
rpc: Rpc,
}
#[derive(ArgEnum, Clone, Debug)]
#[repr(u8)]
pub enum CliSide {
Bid = 0,
Ask = 1,
}
#[derive(Args, Debug, Clone)]
struct PerpPlaceOrder {
#[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(long)]
expiry: u64,
#[clap(flatten)]
rpc: Rpc,
}
#[derive(Subcommand, Debug, Clone)]
enum Command {
CreateAccount(CreateAccount),
@ -128,6 +166,7 @@ enum Command {
#[clap(short, long)]
output: String,
},
PerpPlaceOrder(PerpPlaceOrder),
}
impl Rpc {
@ -248,6 +287,52 @@ async fn main() -> Result<(), anyhow::Error> {
let client = rpc.client(None)?;
save_snapshot::save_snapshot(mango_group, client, output).await?
}
Command::PerpPlaceOrder(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 = client
.context
.perp_markets
.iter()
.find(|p| p.1.name == cmd.market_name)
.unwrap()
.1;
fn native(x: f64, b: u32) -> i64 {
(x * (10_i64.pow(b)) as f64) as i64
}
let price_lots = native(cmd.price, 6) * market.base_lot_size
/ (market.quote_lot_size * 10_i64.pow(market.base_decimals.into()));
let max_base_lots =
native(cmd.quantity, market.base_decimals.into()) / market.base_lot_size;
let txsig = client
.perp_place_order(
market.perp_market_index,
match cmd.side {
CliSide::Bid => Side::Bid,
CliSide::Ask => Side::Ask,
},
price_lots,
max_base_lots,
i64::max_value(),
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
PlaceOrderType::Limit,
false,
if cmd.expiry > 0 {
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + cmd.expiry
} else {
0
},
10,
SelfTradeBehavior::AbortTransaction,
)
.await?;
println!("{}", txsig);
}
};
Ok(())

View File

@ -45,6 +45,7 @@ use solana_sdk::signer::keypair;
use solana_sdk::transaction::TransactionError;
use anyhow::Context;
use mango_v4::error::{IsAnchorErrorWithCode, MangoError};
use solana_sdk::account::ReadableAccount;
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::signature::{Keypair, Signature};
@ -1058,51 +1059,64 @@ impl MangoClient {
limit: u8,
self_trade_behavior: SelfTradeBehavior,
) -> anyhow::Result<PreparedInstructions> {
let mut ixs = PreparedInstructions::new();
let perp = self.context.perp(market_index);
let mut account = account.clone();
let close_perp_ixs_opt = self
.replace_perp_market_if_needed(&account, market_index)
.await?;
if let Some((close_perp_ixs, modified_account)) = close_perp_ixs_opt {
account = modified_account;
ixs.append(close_perp_ixs);
}
let (health_remaining_metas, health_cu) = self
.derive_health_check_remaining_account_metas(
account,
&account,
vec![],
vec![],
vec![market_index],
)
.await?;
let ixs = PreparedInstructions::from_single(
Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpPlaceOrder {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
perp_market: perp.address,
bids: perp.bids,
asks: perp.asks,
event_queue: perp.event_queue,
oracle: perp.oracle,
},
None,
);
ams.extend(health_remaining_metas.into_iter());
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpPlaceOrderV2 {
side,
price_lots,
max_base_lots,
max_quote_lots,
client_order_id,
order_type,
reduce_only,
expiry_timestamp,
limit,
self_trade_behavior,
let ix = Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpPlaceOrder {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
perp_market: perp.address,
bids: perp.bids,
asks: perp.asks,
event_queue: perp.event_queue,
oracle: perp.oracle,
},
),
None,
);
ams.extend(health_remaining_metas.into_iter());
ams
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpPlaceOrderV2 {
side,
price_lots,
max_base_lots,
max_quote_lots,
client_order_id,
order_type,
reduce_only,
expiry_timestamp,
limit,
self_trade_behavior,
}),
};
ixs.push(
ix,
self.instruction_cu(health_cu)
+ self.context.compute_estimates.cu_per_perp_order_match * limit as u32,
);
@ -1110,6 +1124,44 @@ impl MangoClient {
Ok(ixs)
}
async fn replace_perp_market_if_needed(
&self,
account: &MangoAccountValue,
perk_market_index: PerpMarketIndex,
) -> anyhow::Result<Option<(PreparedInstructions, MangoAccountValue)>> {
let context = &self.context;
let settle_token_index = context.perp(perk_market_index).settle_token_index;
let mut account = account.clone();
let enforce_position_result =
account.ensure_perp_position(perk_market_index, settle_token_index);
if !enforce_position_result
.is_anchor_error_with_code(MangoError::NoFreePerpPositionIndex.error_code())
{
return Ok(None);
}
let perp_position_to_close_opt = account.find_first_active_unused_perp_position();
match perp_position_to_close_opt {
Some(perp_position_to_close) => {
let close_ix = self
.perp_deactivate_position_instruction(perp_position_to_close.market_index)
.await?;
let previous_market = context.perp(perp_position_to_close.market_index);
account.deactivate_perp_position(
perp_position_to_close.market_index,
previous_market.settle_token_index,
)?;
account.ensure_perp_position(perk_market_index, settle_token_index)?;
Ok(Some((close_ix, account)))
}
None => anyhow::bail!("No perp market slot available"),
}
}
#[allow(clippy::too_many_arguments)]
pub async fn perp_place_order(
&self,
@ -1182,18 +1234,23 @@ impl MangoClient {
&self,
market_index: PerpMarketIndex,
) -> anyhow::Result<Signature> {
let perp = self.context.perp(market_index);
let mango_account = &self.mango_account().await?;
let (health_check_metas, health_cu) = self
.derive_health_check_remaining_account_metas(mango_account, vec![], vec![], vec![])
let ixs = self
.perp_deactivate_position_instruction(market_index)
.await?;
self.send_and_confirm_owner_tx(ixs.to_instructions()).await
}
async fn perp_deactivate_position_instruction(
&self,
market_index: PerpMarketIndex,
) -> anyhow::Result<PreparedInstructions> {
let perp = self.context.perp(market_index);
let ixs = PreparedInstructions::from_single(
Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
let ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpDeactivatePosition {
group: self.group(),
account: self.mango_account_address,
@ -1202,16 +1259,15 @@ impl MangoClient {
},
None,
);
ams.extend(health_check_metas.into_iter());
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpDeactivatePosition {},
),
},
self.instruction_cu(health_cu),
self.context.compute_estimates.cu_per_mango_instruction,
);
self.send_and_confirm_owner_tx(ixs.to_instructions()).await
Ok(ixs)
}
pub async fn perp_settle_pnl_instruction(

View File

@ -1155,6 +1155,7 @@ impl<
}
}
// Only used in unit tests
pub fn deactivate_perp_position(
&mut self,
perp_market_index: PerpMarketIndex,
@ -1196,6 +1197,19 @@ impl<
Ok(())
}
pub fn find_first_active_unused_perp_position(&self) -> Option<&PerpPosition> {
let first_unused_position_opt = self.all_perp_positions().find(|p| {
p.is_active()
&& p.base_position_lots == 0
&& p.quote_position_native == 0
&& p.bids_base_lots == 0
&& p.asks_base_lots == 0
&& p.taker_base_lots == 0
&& p.taker_quote_lots == 0
});
first_unused_position_opt
}
pub fn add_perp_order(
&mut self,
perp_market_index: PerpMarketIndex,
@ -2808,4 +2822,34 @@ mod tests {
Ok(())
}
#[test]
fn test_perp_auto_close_first_unused() {
let mut account = make_test_account();
// Fill all perp slots
assert_eq!(account.header.perp_count, 4);
account.ensure_perp_position(1, 0).unwrap();
account.ensure_perp_position(2, 0).unwrap();
account.ensure_perp_position(3, 0).unwrap();
account.ensure_perp_position(4, 0).unwrap();
assert_eq!(account.active_perp_positions().count(), 4);
// Force usage of some perp slot (leaves 3 unused)
account.perp_position_mut(1).unwrap().taker_base_lots = 10;
account.perp_position_mut(2).unwrap().base_position_lots = 10;
account.perp_position_mut(4).unwrap().quote_position_native = I80F48::from_num(10);
assert!(account.perp_position(3).ok().is_some());
// Should not succeed anymore
{
let e = account.ensure_perp_position(5, 0);
assert!(e.is_anchor_error_with_code(MangoError::NoFreePerpPositionIndex.error_code()));
}
// Act
let to_be_closed_account_opt = account.find_first_active_unused_perp_position();
assert_eq!(to_be_closed_account_opt.unwrap().market_index, 3)
}
}