Merge remote-tracking branch 'origin/dev' into ts/orca-margin-trade

This commit is contained in:
tjs 2022-05-31 15:57:26 -04:00
commit 4c5523c95b
33 changed files with 2356 additions and 754 deletions

View File

@ -9,9 +9,9 @@ on:
env:
CARGO_TERM_COLOR: always
SOLANA_VERSION: "1.9.14"
SOLANA_VERSION: '1.9.14'
RUST_TOOLCHAIN: 1.60.0
LOG_PROGRAM: "m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD"
LOG_PROGRAM: 'm43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD'
defaults:
run:
@ -111,4 +111,3 @@ jobs:
with:
name: cu-per-ix-clean
path: cu-per-ix-clean.log

5
Cargo.lock generated
View File

@ -1710,13 +1710,18 @@ version = "0.1.0"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-spl",
"anyhow",
"clap 3.1.18",
"dotenv",
"env_logger 0.8.4",
"fixed",
"fixed-macro",
"futures",
"log",
"mango-v4",
"pyth-sdk-solana",
"serum_dex",
"solana-client",
"solana-sdk",
"tokio",

View File

@ -1,97 +0,0 @@
2022-05-24,Benchmark,9227
2022-05-24,CloseAccount,4758
2022-05-24,CreateAccount,25781
2022-05-24,CreateGroup,15147
2022-05-24,CreateGroup,16647
2022-05-24,CreateStubOracle,21810
2022-05-24,CreateStubOracle,24810
2022-05-24,CreateStubOracle,5129
2022-05-24,CreateStubOracle,51734
2022-05-24,CreateStubOracle,56202
2022-05-24,CreateStubOracle,59202
2022-05-24,Deposit,20310
2022-05-24,Deposit,26053
2022-05-24,Deposit,26565
2022-05-24,InitializeAccount,16936
2022-05-24,InitializeAccount,34110
2022-05-24,InitializeAccount,36061
2022-05-24,InitializeAccount,38114
2022-05-24,InitializeAccount,44193
2022-05-24,InitializeAccount,56202
2022-05-24,InitializeAccount,57693
2022-05-24,InitializeAccount,63702
2022-05-24,InitializeAccount,66693
2022-05-24,LiqTokenWithToken,57875
2022-05-24,LiqTokenWithToken,59885
2022-05-24,LiqTokenWithToken,61768
2022-05-24,PerpCancelAllOrders,11415
2022-05-24,PerpCancelOrder,8951
2022-05-24,PerpCancelOrderByClientOrderId,8958
2022-05-24,PerpConsumeEvents,6388
2022-05-24,PerpCreateMarket,26251
2022-05-24,PerpPlaceOrder,20751
2022-05-24,PerpPlaceOrder,20884
2022-05-24,PerpPlaceOrder,21451
2022-05-24,PerpPlaceOrder,22082
2022-05-24,PerpPlaceOrder,23137
2022-05-24,RegisterToken,17310
2022-05-24,RegisterToken,5129
2022-05-24,RegisterToken,53193
2022-05-24,RegisterToken,53202
2022-05-24,RegisterToken,54693
2022-05-24,Revoke,93216
2022-05-24,Revoke,97269
2022-05-24,Revoke,97678
2022-05-24,Serum3CancelOrder,24472
2022-05-24,Serum3CreateOpenOrders,26470
2022-05-24,Serum3CreateOpenOrders,26493
2022-05-24,Serum3CreateOpenOrders,26558
2022-05-24,Serum3CreateOpenOrders,27873
2022-05-24,Serum3CreateOpenOrders,27965
2022-05-24,Serum3CreateOpenOrders,27975
2022-05-24,Serum3CreateOpenOrders,27987
2022-05-24,Serum3CreateOpenOrders,29481
2022-05-24,Serum3CreateOpenOrders,29500
2022-05-24,Serum3RegisterMarket,24281
2022-05-24,Serum3RegisterMarket,27281
2022-05-24,SetStubOracle,17310
2022-05-24,Transfer,22297
2022-05-24,Transfer,22347
2022-05-24,Transfer,25862
2022-05-24,Transfer,25910
2022-05-24,Transfer,26390
2022-05-24,Transfer,26565
2022-05-24,Transfer,26708
2022-05-24,Transfer,27631
2022-05-24,Transfer,27793
2022-05-24,Transfer,27829
2022-05-24,Transfer,27850
2022-05-24,Transfer,28554
2022-05-24,Transfer,28695
2022-05-24,Transfer,28761
2022-05-24,Transfer,29298
2022-05-24,Transfer,29883
2022-05-24,Transfer,29977
2022-05-24,Transfer,30194
2022-05-24,Transfer,30492
2022-05-24,Transfer,30632
2022-05-24,Transfer,30851
2022-05-24,Transfer,31374
2022-05-24,Transfer,31927
2022-05-24,Transfer,32444
2022-05-24,Transfer,32708
2022-05-24,Transfer,35581
2022-05-24,Transfer,40066
2022-05-24,Transfer,40535
2022-05-24,Transfer,42249
2022-05-24,Transfer,45726
2022-05-24,Transfer,48659
2022-05-24,Transfer,50696
2022-05-24,Transfer,55672
2022-05-24,Transfer,60658
2022-05-24,Transfer,65986
2022-05-24,Transfer,76337
2022-05-24,Transfer,89560
2022-05-24,Transfer,89889
2022-05-24,UpdateIndex,20940
2022-05-24,Withdraw,33238

View File

@ -1,3 +1,4 @@
RPC_URL=
PAYER_KEYPAIR=
ADMIN_KEYPAIR=
MANGO_ACCOUNT_NAME=

View File

@ -8,16 +8,18 @@ edition = "2021"
[dependencies]
anchor-client = "0.24.2"
anchor-lang = "0.24.2"
anchor-spl = "0.24.2"
anyhow = "1.0"
clap = { version = "3.1.8", features = ["derive", "env"] }
dotenv = "0.15.0"
env_logger = "0.8.4"
fixed = { version = "=1.11.0", features = ["serde", "borsh"] }
fixed-macro = "^1.1.1"
futures = "0.3.21"
log = "0.4.0"
mango-v4 = { path = "../programs/mango-v4" }
# serde = { version = "1.0", features = ["derive"] }
# serde_json = "1.0"
# shellexpand = "2.1"
pyth-sdk-solana = "0.1.0"
serum_dex = { version = "0.4.0", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false, features = ["no-entrypoint", "program"] }
solana-client = "~1.9.13"
solana-sdk = "~1.9.13"
tokio = { version = "1.18.2", features = ["rt-multi-thread", "time", "macros", "sync"] }

View File

@ -1,125 +0,0 @@
use std::{sync::Arc, time::Duration};
use anchor_lang::{AccountDeserialize, __private::bytemuck::cast_ref};
use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, PerpMarket};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use tokio::time;
use crate::MangoClient;
pub async fn loop_blocking(mango_client: Arc<MangoClient>, pk: Pubkey, perp_market: PerpMarket) {
let mut interval = time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
let client = mango_client.clone();
tokio::task::spawn_blocking(move || {
perform_operation(client, pk, perp_market).expect("Something went wrong here...");
})
.await
.expect("Something went wrong here...");
}
}
pub fn perform_operation(
mango_client: Arc<MangoClient>,
pk: Pubkey,
perp_market: PerpMarket,
) -> anyhow::Result<()> {
let mut event_queue = match get_event_queue(&mango_client, &perp_market) {
Ok(value) => value,
Err(value) => return value,
};
let mut ams_ = vec![];
// TODO: future, choose better constant of how many max events to pack
// TODO: future, choose better constant of how many max mango accounts to pack
for _ in 0..10 {
let event = match event_queue.peek_front() {
None => break,
Some(e) => e,
};
match EventType::try_from(event.event_type)? {
EventType::Fill => {
let fill: &FillEvent = cast_ref(event);
ams_.push(AccountMeta {
pubkey: fill.maker,
is_signer: false,
is_writable: true,
});
ams_.push(AccountMeta {
pubkey: fill.taker,
is_signer: false,
is_writable: true,
});
}
EventType::Out => {
let out: &OutEvent = cast_ref(event);
ams_.push(AccountMeta {
pubkey: out.owner,
is_signer: false,
is_writable: true,
});
}
EventType::Liquidate => {}
}
event_queue.pop_front()?;
}
let sig_result = mango_client
.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpConsumeEvents {
group: perp_market.group,
perp_market: pk,
event_queue: perp_market.event_queue,
},
None,
);
ams.append(&mut ams_);
ams
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpConsumeEvents {
limit: 10,
}),
})
.send();
match sig_result {
Ok(sig) => {
log::info!(
"Crank: consume event for perp_market {:?} ix signature: {:?}",
format!("{: >6}", perp_market.name()),
sig
);
}
Err(e) => log::error!("Crank: {:?}", e),
}
Ok(())
}
fn get_event_queue(
mango_client: &MangoClient,
perp_market: &PerpMarket,
) -> Result<mango_v4::state::EventQueue, Result<(), anyhow::Error>> {
let event_queue_opt: Option<EventQueue> = {
let res = mango_client
.rpc
.get_account_with_commitment(&perp_market.event_queue, mango_client.commitment);
let data = res.unwrap().value.unwrap().data;
let mut data_slice: &[u8] = &data;
AccountDeserialize::try_deserialize(&mut data_slice).ok()
};
let event_queue = event_queue_opt.unwrap();
Ok(event_queue)
}

View File

@ -1,81 +1,36 @@
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
use crate::{consume_events, update_funding, update_index, MangoClient};
use anyhow::ensure;
use crate::MangoClient;
use anchor_lang::__private::bytemuck::cast_ref;
use futures::Future;
use mango_v4::state::{Bank, PerpMarket};
use solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType};
use solana_sdk::{pubkey::Pubkey, signer::Signer};
use mango_v4::state::{Bank, EventQueue, EventType, FillEvent, OutEvent, PerpMarket};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use tokio::time;
pub async fn runner(
mango_client: Arc<MangoClient>,
debugging_handle: impl Future,
) -> Result<(), anyhow::Error> {
// Collect all banks for a group belonging to an admin
let banks = mango_client
.program()
.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 24,
bytes: MemcmpEncodedBytes::Base58({
// find group belonging to admin
Pubkey::find_program_address(
&["Group".as_ref(), mango_client.admin.pubkey().as_ref()],
&mango_client.program().id(),
)
.0
.to_string()
}),
encoding: None,
})])?;
ensure!(!banks.is_empty());
let handles1 = banks
.iter()
.map(|(pk, bank)| update_index::loop_blocking(mango_client.clone(), *pk, *bank))
let handles1 = mango_client
.banks_cache
.values()
.map(|(pk, bank)| loop_update_index(mango_client.clone(), *pk, *bank))
.collect::<Vec<_>>();
// TODO: future, maybe we want to only consume events for specific markets,
// TODO: future, maybe we want to crank certain markets more often than others
// Collect all perp markets for a group belonging to an admin
let perp_markets =
mango_client
.program()
.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 24,
bytes: MemcmpEncodedBytes::Base58({
// find group belonging to admin
Pubkey::find_program_address(
&["Group".as_ref(), mango_client.admin.pubkey().as_ref()],
&mango_client.program().id(),
)
.0
.to_string()
}),
encoding: None,
})])?;
// TODO: enable
// ensure!(!perp_markets.is_empty());
// atm no perp code is deployed to devnet, and no perp markets have been init
let handles2 = perp_markets
.iter()
.map(|(pk, perp_market)| {
consume_events::loop_blocking(mango_client.clone(), *pk, *perp_market)
})
let handles2 = mango_client
.perp_markets_cache
.values()
.map(|(pk, perp_market)| loop_consume_events(mango_client.clone(), *pk, *perp_market))
.collect::<Vec<_>>();
let handles3 = perp_markets
.iter()
.map(|(pk, perp_market)| {
update_funding::loop_blocking(mango_client.clone(), *pk, *perp_market)
})
let handles3 = mango_client
.perp_markets_cache
.values()
.map(|(pk, perp_market)| loop_update_funding(mango_client.clone(), *pk, *perp_market))
.collect::<Vec<_>>();
futures::join!(
@ -87,3 +42,204 @@ pub async fn runner(
Ok(())
}
pub async fn loop_update_index(mango_client: Arc<MangoClient>, pk: Pubkey, bank: Bank) {
let mut interval = time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
let client = mango_client.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let sig_result = client
.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::UpdateIndex { bank: pk },
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::UpdateIndex {},
),
})
.send();
if let Err(e) = sig_result {
log::error!("{:?}", e)
} else {
log::info!("update_index {} {:?}", bank.name(), sig_result.unwrap())
}
Ok(())
})
.await;
match res {
Ok(inner_res) => {
if inner_res.is_err() {
log::error!("{}", inner_res.unwrap_err());
}
}
Err(join_error) => {
log::error!("{}", join_error);
}
}
}
}
pub async fn loop_consume_events(
mango_client: Arc<MangoClient>,
pk: Pubkey,
perp_market: PerpMarket,
) {
let mut interval = time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
let client = mango_client.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let mut event_queue: EventQueue =
client.program().account(perp_market.event_queue).unwrap();
let mut ams_ = vec![];
// TODO: future, choose better constant of how many max events to pack
// TODO: future, choose better constant of how many max mango accounts to pack
for _ in 0..10 {
let event = match event_queue.peek_front() {
None => break,
Some(e) => e,
};
match EventType::try_from(event.event_type)? {
EventType::Fill => {
let fill: &FillEvent = cast_ref(event);
ams_.push(AccountMeta {
pubkey: fill.maker,
is_signer: false,
is_writable: true,
});
ams_.push(AccountMeta {
pubkey: fill.taker,
is_signer: false,
is_writable: true,
});
}
EventType::Out => {
let out: &OutEvent = cast_ref(event);
ams_.push(AccountMeta {
pubkey: out.owner,
is_signer: false,
is_writable: true,
});
}
EventType::Liquidate => {}
}
event_queue.pop_front()?;
}
let sig_result = client
.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpConsumeEvents {
group: perp_market.group,
perp_market: pk,
event_queue: perp_market.event_queue,
},
None,
);
ams.append(&mut ams_);
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpConsumeEvents { limit: 10 },
),
})
.send();
if let Err(e) = sig_result {
log::error!("{:?}", e)
} else {
log::info!(
"consume_event {} {:?}",
perp_market.name(),
sig_result.unwrap()
)
}
Ok(())
})
.await;
match res {
Ok(inner_res) => {
if inner_res.is_err() {
log::error!("{}", inner_res.unwrap_err());
}
}
Err(join_error) => {
log::error!("{}", join_error);
}
}
}
}
pub async fn loop_update_funding(
mango_client: Arc<MangoClient>,
pk: Pubkey,
perp_market: PerpMarket,
) {
let mut interval = time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
let client = mango_client.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let sig_result = client
.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpUpdateFunding {
perp_market: pk,
asks: perp_market.asks,
bids: perp_market.bids,
oracle: perp_market.oracle,
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpUpdateFunding {},
),
})
.send();
if let Err(e) = sig_result {
log::error!("{:?}", e)
} else {
log::info!(
"update_funding {} {:?}",
perp_market.name(),
sig_result.unwrap()
)
}
Ok(())
})
.await;
match res {
Ok(inner_res) => {
if inner_res.is_err() {
log::error!("{}", inner_res.unwrap_err());
}
}
Err(join_error) => {
log::error!("{}", join_error);
}
}
}
}

View File

@ -1,25 +1,20 @@
mod consume_events;
mod crank;
mod update_funding;
mod update_index;
mod mango_client;
mod taker;
mod util;
use std::env;
use std::sync::Arc;
use anchor_client::{Client, Cluster, Program};
use anchor_client::Cluster;
use clap::{Parser, Subcommand};
use solana_client::rpc_client::RpcClient;
use solana_sdk::signature::Keypair;
use solana_sdk::{
commitment_config::CommitmentConfig,
pubkey::Pubkey,
signer::{keypair, Signer},
};
use solana_sdk::{commitment_config::CommitmentConfig, signer::keypair};
use tokio::time;
use crate::mango_client::MangoClient;
// TODO
// - may be nice to have one-shot cranking as well as the interval cranking
// - doing a gPA for all banks call every 10millis may be too often,
@ -27,77 +22,20 @@ use tokio::time;
// - I'm really annoyed about Keypair not being clonable. Seems everyone works around that manually. Should make a PR to solana to newtype it and provide that function.
// keypair_from_arg_or_env could be a function
/// Wrapper around anchor client with some mango specific useful things
pub struct MangoClient {
pub rpc: RpcClient,
pub cluster: Cluster,
pub commitment: CommitmentConfig,
pub payer: Keypair,
pub admin: Keypair,
}
impl MangoClient {
pub fn new(
cluster: Cluster,
commitment: CommitmentConfig,
payer: Keypair,
admin: Keypair,
) -> Self {
let program = Client::new_with_options(
cluster.clone(),
std::rc::Rc::new(Keypair::from_bytes(&payer.to_bytes()).unwrap()),
commitment,
)
.program(mango_v4::ID);
let rpc = program.rpc();
Self {
rpc,
cluster,
commitment,
admin,
payer,
}
}
pub fn client(&self) -> Client {
Client::new_with_options(
self.cluster.clone(),
std::rc::Rc::new(Keypair::from_bytes(&self.payer.to_bytes()).unwrap()),
self.commitment,
)
}
pub fn program(&self) -> Program {
self.client().program(mango_v4::ID)
}
pub fn payer(&self) -> Pubkey {
self.payer.pubkey()
}
pub fn admin(&self) -> Pubkey {
self.payer.pubkey()
}
}
#[derive(Parser)]
#[clap()]
struct Cli {
#[clap(long, env = "RPC_URL")]
#[clap(short, long, env = "RPC_URL")]
rpc_url: Option<String>,
#[clap(long, env = "PAYER_KEYPAIR")]
#[clap(short, long, env = "PAYER_KEYPAIR")]
payer: Option<std::path::PathBuf>,
#[clap(long, env = "PAYER_KEYPAIR_BASE58")]
payer_base58: Option<String>,
#[clap(long, env = "ADMIN_KEYPAIR")]
#[clap(short, long, env = "ADMIN_KEYPAIR")]
admin: Option<std::path::PathBuf>,
#[clap(long, env = "ADMIN_KEYPAIR_BASE58")]
admin_base58: Option<String>,
#[clap(short, long, env = "MANGO_ACCOUNT_NAME")]
mango_account_name: String,
#[clap(subcommand)]
command: Command,
@ -106,7 +44,7 @@ struct Cli {
#[derive(Subcommand)]
enum Command {
Crank {},
Liquidator {},
Taker {},
}
fn main() -> Result<(), anyhow::Error> {
env_logger::init_from_env(
@ -118,44 +56,21 @@ fn main() -> Result<(), anyhow::Error> {
let Cli {
rpc_url,
payer,
payer_base58,
admin,
admin_base58,
command,
mango_account_name,
} = Cli::parse();
let payer = {
if let Some(base58_string) = payer_base58 {
Keypair::from_base58_string(&base58_string)
} else {
match payer {
Some(p) => keypair::read_keypair_file(&p).unwrap_or_else(|_| {
panic!("Failed to read keypair from {}", p.to_string_lossy())
}),
None => match env::var("PAYER_KEYPAIR").ok() {
Some(k) => keypair::read_keypair(&mut k.as_bytes())
.expect("Failed to parse $PAYER_KEYPAIR"),
None => panic!("Payer keypair not provided..."),
},
}
}
let payer = match payer {
Some(p) => keypair::read_keypair_file(&p)
.unwrap_or_else(|_| panic!("Failed to read keypair from {}", p.to_string_lossy())),
None => panic!("Payer keypair not provided..."),
};
let admin = {
if let Some(base58_string) = admin_base58 {
Keypair::from_base58_string(&base58_string)
} else {
match admin {
Some(p) => keypair::read_keypair_file(&p).unwrap_or_else(|_| {
panic!("Failed to read keypair from {}", p.to_string_lossy())
}),
None => match env::var("ADMIN_KEYPAIR").ok() {
Some(k) => keypair::read_keypair(&mut k.as_bytes())
.expect("Failed to parse $ADMIN_KEYPAIR"),
None => panic!("Admin keypair not provided..."),
},
}
}
let admin = match admin {
Some(p) => keypair::read_keypair_file(&p)
.unwrap_or_else(|_| panic!("Failed to read keypair from {}", p.to_string_lossy())),
None => panic!("Admin keypair not provided..."),
};
let rpc_url = match rpc_url {
@ -170,20 +85,22 @@ fn main() -> Result<(), anyhow::Error> {
let cluster = Cluster::Custom(rpc_url, ws_url);
let commitment = match command {
Command::Crank { .. } => CommitmentConfig::confirmed(),
Command::Liquidator {} => todo!(),
Command::Taker { .. } => CommitmentConfig::confirmed(),
};
let mango_client = Arc::new(MangoClient::new(cluster, commitment, payer, admin));
log::info!("Program Id {}", &mango_client.program().id());
log::info!("Admin {}", &mango_client.admin.to_base58_string());
let mango_client = Arc::new(MangoClient::new(
cluster,
commitment,
payer,
admin,
mango_account_name,
)?);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
// TODO: future: remove, just for learning purposes
let debugging_handle = async {
let mut interval = time::interval(time::Duration::from_secs(5));
loop {
@ -191,7 +108,7 @@ fn main() -> Result<(), anyhow::Error> {
let client = mango_client.clone();
tokio::task::spawn_blocking(move || {
log::info!(
"std::sync::Arc<MangoClient>::strong_count() {}",
"Arc<MangoClient>::strong_count() {}",
Arc::<MangoClient>::strong_count(&client)
)
});
@ -201,13 +118,11 @@ fn main() -> Result<(), anyhow::Error> {
match command {
Command::Crank { .. } => {
let client = mango_client.clone();
let x: Result<(), anyhow::Error> = rt.block_on(crank::runner(client, debugging_handle));
x.expect("Something went wrong here...");
rt.block_on(crank::runner(client, debugging_handle))
}
Command::Liquidator { .. } => {
todo!()
Command::Taker { .. } => {
let client = mango_client.clone();
rt.block_on(taker::runner(client, debugging_handle))
}
}
Ok(())
}

780
keeper/src/mango_client.rs Normal file
View File

@ -0,0 +1,780 @@
use std::collections::HashMap;
use anchor_client::{Client, Cluster, Program};
use anchor_lang::__private::bytemuck;
use anchor_lang::prelude::System;
use anchor_lang::{AccountDeserialize, Id};
use anchor_spl::associated_token::get_associated_token_address;
use anchor_spl::token::{Mint, Token};
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::state::{Bank, MangoAccount, MintInfo, PerpMarket, Serum3Market, TokenIndex};
use solana_client::rpc_client::RpcClient;
use crate::util::MyClone;
use anyhow::Context;
use solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType};
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};
pub struct MangoClient {
pub rpc: RpcClient,
pub cluster: Cluster,
pub commitment: CommitmentConfig,
pub payer: Keypair,
pub admin: Keypair,
pub mango_account_cache: (Pubkey, MangoAccount),
pub group: Pubkey,
// TODO: future: this may not scale if there's thousands of mints, probably some function
// wrapping getMultipleAccounts is needed (or bettew: we provide this data as a service)
pub banks_cache: HashMap<String, (Pubkey, Bank)>,
pub banks_cache_by_token_index: HashMap<TokenIndex, (Pubkey, Bank)>,
pub mint_infos_cache: HashMap<Pubkey, (Pubkey, MintInfo, Mint)>,
pub mint_infos_cache_by_token_index: HashMap<TokenIndex, (Pubkey, MintInfo, Mint)>,
pub serum3_markets_cache: HashMap<String, (Pubkey, Serum3Market)>,
pub serum3_external_markets_cache: HashMap<String, (Pubkey, Vec<u8>)>,
pub perp_markets_cache: HashMap<String, (Pubkey, PerpMarket)>,
}
// 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 {
pub fn new(
cluster: Cluster,
commitment: CommitmentConfig,
payer: Keypair,
admin: Keypair,
mango_account_name: String,
) -> 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();
let group = Pubkey::find_program_address(
&[
"Group".as_ref(),
admin.pubkey().as_ref(),
0u32.to_le_bytes().as_ref(),
],
&program.id(),
)
.0;
log::info!("Program Id {}", program.id());
log::info!("Admin {}", admin.pubkey());
log::info!("Group {}", group);
log::info!("User {}", payer.pubkey());
// Mango Account
let mut mango_account_tuples = program.accounts::<MangoAccount>(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 40,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 72,
bytes: MemcmpEncodedBytes::Base58(payer.pubkey().to_string()),
encoding: None,
}),
])?;
let mango_account_opt = mango_account_tuples
.iter()
.find(|tuple| tuple.1.name() == mango_account_name);
if mango_account_opt.is_none() {
mango_account_tuples
.sort_by(|a, b| a.1.account_num.partial_cmp(&b.1.account_num).unwrap());
let account_num = match mango_account_tuples.last() {
Some(tuple) => tuple.1.account_num + 1,
None => 0u8,
};
program
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::CreateAccount {
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(
&mango_v4::instruction::CreateAccount {
account_num,
name: mango_account_name.to_owned(),
},
),
})
.send()
.context("Failed to create account...")?;
}
let mango_account_tuples = program.accounts::<MangoAccount>(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 40,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 72,
bytes: MemcmpEncodedBytes::Base58(payer.pubkey().to_string()),
encoding: None,
}),
])?;
let index = mango_account_tuples
.iter()
.position(|tuple| tuple.1.name() == &mango_account_name)
.unwrap();
let mango_account_cache = mango_account_tuples[index];
// banks cache
let mut banks_cache = HashMap::new();
let mut banks_cache_by_token_index = HashMap::new();
let bank_tuples = program.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 24,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
for (k, v) in bank_tuples {
banks_cache.insert(v.name().to_owned(), (k, v));
banks_cache_by_token_index.insert(v.token_index, (k, v));
}
// mintinfo cache
let mut mint_infos_cache = HashMap::new();
let mut mint_infos_cache_by_token_index = HashMap::new();
let mint_info_tuples =
program.accounts::<MintInfo>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
for (k, v) in mint_info_tuples {
let data = program
.rpc()
.get_account_with_commitment(&v.mint, commitment)?
.value
.unwrap()
.data;
let mint = Mint::try_deserialize(&mut &data[..])?;
mint_infos_cache.insert(v.mint, (k, v, mint.clone()));
mint_infos_cache_by_token_index.insert(v.token_index, (k, v, mint));
}
// serum3 markets cache
let mut serum3_markets_cache = HashMap::new();
let mut serum3_external_markets_cache = HashMap::new();
let serum3_market_tuples =
program.accounts::<Serum3Market>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 24,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
for (k, v) in serum3_market_tuples {
serum3_markets_cache.insert(v.name().to_owned(), (k, v));
let market_external_bytes = program
.rpc()
.get_account_with_commitment(&v.serum_market_external, commitment)?
.value
.unwrap()
.data;
serum3_external_markets_cache.insert(
v.name().to_owned(),
(v.serum_market_external, market_external_bytes),
);
}
// perp markets cache
let mut perp_markets_cache = HashMap::new();
let perp_market_tuples =
program.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 24,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
for (k, v) in perp_market_tuples {
perp_markets_cache.insert(v.name().to_owned(), (k, v));
}
Ok(Self {
rpc,
cluster,
commitment,
admin,
payer,
mango_account_cache,
group,
banks_cache,
banks_cache_by_token_index,
mint_infos_cache,
mint_infos_cache_by_token_index,
serum3_markets_cache,
serum3_external_markets_cache,
perp_markets_cache,
})
}
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 {
self.group
}
pub fn get_account(&self) -> Result<(Pubkey, MangoAccount), anchor_client::ClientError> {
let mango_accounts = self.program().accounts::<MangoAccount>(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 40,
bytes: MemcmpEncodedBytes::Base58(self.group().to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 72,
bytes: MemcmpEncodedBytes::Base58(self.payer().to_string()),
encoding: None,
}),
])?;
Ok(mango_accounts[0])
}
pub fn derive_health_check_remaining_account_metas(
&self,
affected_bank: Option<(Pubkey, Bank)>,
writable_banks: bool,
) -> Result<Vec<AccountMeta>, anchor_client::ClientError> {
// figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![];
let mut oracles = vec![];
let account = self.get_account()?;
for position in account.1.tokens.iter_active() {
let mint_info = self
.mint_infos_cache_by_token_index
.get(&position.token_index)
.unwrap()
.1;
// TODO: ALTs are unavailable
// let lookup_table = account_loader
// .load_bytes(&mint_info.address_lookup_table)
// .await
// .unwrap();
// let addresses = mango_v4::address_lookup_table::addresses(&lookup_table);
// banks.push(addresses[mint_info.address_lookup_table_bank_index as usize]);
// oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]);
banks.push(mint_info.bank);
oracles.push(mint_info.oracle);
}
if let Some(affected_bank) = affected_bank {
if !banks.iter().any(|&v| v == affected_bank.0) {
// If there is not yet an active position for the token, we need to pass
// the bank/oracle for health check anyway.
let new_position = account
.1
.tokens
.values
.iter()
.position(|p| !p.is_active())
.unwrap();
banks.insert(new_position, affected_bank.0);
oracles.insert(new_position, affected_bank.1.oracle);
}
}
let serum_oos = account.1.serum3.iter_active().map(|&s| s.open_orders);
Ok(banks
.iter()
.map(|&pubkey| AccountMeta {
pubkey,
is_writable: writable_banks,
is_signer: false,
})
.chain(oracles.iter().map(|&pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.chain(serum_oos.map(|pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.collect())
}
pub fn deposit(
&self,
token_name: &str,
amount: u64,
) -> Result<Signature, anchor_client::ClientError> {
let bank = self.banks_cache.get(token_name).unwrap();
let mint_info: MintInfo = self.mint_infos_cache.get(&bank.1.mint).unwrap().1;
let health_check_metas =
self.derive_health_check_remaining_account_metas(Some(*bank), false)?;
self.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::Deposit {
group: self.group(),
account: self.mango_account_cache.0,
bank: bank.0,
vault: bank.1.vault,
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
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::Deposit {
amount,
}),
})
.send()
}
pub fn get_oracle_price(
&self,
token_name: &str,
) -> Result<pyth_sdk_solana::Price, anyhow::Error> {
let bank = self.banks_cache.get(token_name).unwrap().1;
let data = self
.program()
.rpc()
.get_account_with_commitment(&bank.oracle, self.commitment)?
.value
.unwrap()
.data;
Ok(pyth_sdk_solana::load_price(&data).unwrap())
}
//
// Serum3
//
pub fn serum3_create_open_orders(
&self,
name: &str,
) -> Result<Signature, anchor_client::ClientError> {
let (account_pubkey, _) = self.mango_account_cache;
let serum3_market = self.serum3_markets_cache.get(name).unwrap();
let open_orders = Pubkey::find_program_address(
&[
account_pubkey.as_ref(),
b"Serum3OO".as_ref(),
serum3_market.0.as_ref(),
],
&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,
serum_market: serum3_market.0,
serum_program: serum3_market.1.serum_program,
serum_market_external: serum3_market.1.serum_market_external,
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()
}
#[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,
) -> Result<Signature, anchor_client::ClientError> {
let (_, account) = self.get_account()?;
let serum3_market = self.serum3_markets_cache.get(name).unwrap();
let open_orders = account
.serum3
.find(serum3_market.1.market_index)
.unwrap()
.open_orders;
let (_, quote_info, quote_mint) = self
.mint_infos_cache_by_token_index
.get(&serum3_market.1.quote_token_index)
.unwrap();
let (_, base_info, base_mint) = self
.mint_infos_cache_by_token_index
.get(&serum3_market.1.base_token_index)
.unwrap();
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
&(self.serum3_external_markets_cache.get(name).unwrap().1)
[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
);
let bids = market_external.bids;
let asks = market_external.asks;
let event_q = market_external.event_q;
let req_q = market_external.req_q;
let coin_vault = market_external.coin_vault;
let pc_vault = market_external.pc_vault;
let vault_signer = serum_dex::state::gen_vault_signer_key(
market_external.vault_signer_nonce,
&serum3_market.1.serum_market_external,
&serum3_market.1.serum_program,
)
.unwrap();
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 = {
(price
* ((10u64.pow(quote_mint.decimals as u32) * market_external.coin_lot_size) as f64))
as u64
/ (10u64.pow(base_mint.decimals as u32) * market_external.pc_lot_size)
};
// https://github.com/project-serum/serum-ts/blob/master/packages/serum/src/market.ts#L1333
let max_base_qty = {
(size * 10u64.pow(base_mint.decimals as u32) as f64) as u64
/ market_external.coin_lot_size
};
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);
(market_external.pc_lot_size as f64 * (1f64 + rates.0)) as u64
* (limit_price * max_base_qty)
};
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(),
account: self.mango_account_cache.0,
open_orders,
quote_bank: quote_info.bank,
quote_vault: quote_info.vault,
base_bank: base_info.bank,
base_vault: base_info.vault,
serum_market: serum3_market.0,
serum_program: serum3_market.1.serum_program,
serum_market_external: serum3_market.1.serum_market_external,
market_bids: from_serum_style_pubkey(&bids),
market_asks: from_serum_style_pubkey(&asks),
market_event_queue: from_serum_style_pubkey(&event_q),
market_request_queue: from_serum_style_pubkey(&req_q),
market_base_vault: from_serum_style_pubkey(&coin_vault),
market_quote_vault: from_serum_style_pubkey(&pc_vault),
market_vault_signer: vault_signer,
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()
}
pub fn serum3_settle_funds(&self, name: &str) -> Result<Signature, anchor_client::ClientError> {
let (_, account) = self.get_account()?;
let serum3_market = self.serum3_markets_cache.get(name).unwrap();
let open_orders = account
.serum3
.find(serum3_market.1.market_index)
.unwrap()
.open_orders;
let (_, quote_info, _) = self
.mint_infos_cache_by_token_index
.get(&serum3_market.1.quote_token_index)
.unwrap();
let (_, base_info, _) = self
.mint_infos_cache_by_token_index
.get(&serum3_market.1.base_token_index)
.unwrap();
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
&(self.serum3_external_markets_cache.get(name).unwrap().1)
[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
);
let coin_vault = market_external.coin_vault;
let pc_vault = market_external.pc_vault;
let vault_signer = serum_dex::state::gen_vault_signer_key(
market_external.vault_signer_nonce,
&serum3_market.1.serum_market_external,
&serum3_market.1.serum_program,
)
.unwrap();
self.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::Serum3SettleFunds {
group: self.group(),
account: self.mango_account_cache.0,
open_orders,
quote_bank: quote_info.bank,
quote_vault: quote_info.vault,
base_bank: base_info.bank,
base_vault: base_info.vault,
serum_market: serum3_market.0,
serum_program: serum3_market.1.serum_program,
serum_market_external: serum3_market.1.serum_market_external,
market_base_vault: from_serum_style_pubkey(&coin_vault),
market_quote_vault: from_serum_style_pubkey(&pc_vault),
market_vault_signer: vault_signer,
owner: self.payer(),
token_program: Token::id(),
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::Serum3SettleFunds {},
),
})
.send()
}
pub fn serum3_cancel_all_orders(&self, market_name: &str) -> Result<Vec<u128>, anyhow::Error> {
let serum3_market = self.serum3_markets_cache.get(market_name).unwrap();
let open_orders = Pubkey::find_program_address(
&[
self.mango_account_cache.0.as_ref(),
b"Serum3OO".as_ref(),
serum3_market.0.as_ref(),
],
&self.program().id(),
)
.0;
let open_orders_bytes = self
.program()
.rpc()
.get_account_with_commitment(&open_orders, self.commitment)?
.value
.unwrap()
.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 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,
) -> Result<(), anyhow::Error> {
let (account_pubkey, _account) = self.get_account()?;
let serum3_market = self.serum3_markets_cache.get(market_name).unwrap();
let open_orders = Pubkey::find_program_address(
&[
account_pubkey.as_ref(),
b"Serum3OO".as_ref(),
serum3_market.0.as_ref(),
],
&self.program().id(),
)
.0;
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
&(self
.serum3_external_markets_cache
.get(market_name)
.unwrap()
.1)[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
);
let bids = market_external.bids;
let asks = market_external.asks;
let event_q = market_external.event_q;
self.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: {
anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::Serum3CancelOrder {
group: self.group(),
account: account_pubkey,
serum_market: serum3_market.0,
serum_program: serum3_market.1.serum_program,
serum_market_external: serum3_market.1.serum_market_external,
open_orders,
market_bids: from_serum_style_pubkey(&bids),
market_asks: from_serum_style_pubkey(&asks),
market_event_queue: from_serum_style_pubkey(&event_q),
owner: self.payer(),
},
None,
)
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::Serum3CancelOrder { side, order_id },
),
})
.send()?;
Ok(())
}
//
// Perps
//
//
//
//
}
fn from_serum_style_pubkey(d: &[u64; 4]) -> Pubkey {
Pubkey::new(bytemuck::cast_slice(d as &[_]))
}

238
keeper/src/taker.rs Normal file
View File

@ -0,0 +1,238 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use fixed::types::I80F48;
use futures::Future;
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use tokio::time;
use crate::MangoClient;
pub async fn runner(
mango_client: Arc<MangoClient>,
_debugging_handle: impl Future,
) -> Result<(), anyhow::Error> {
ensure_deposit(&mango_client)?;
ensure_oo(&mango_client)?;
let mut price_arcs = HashMap::new();
for market_name in mango_client.serum3_markets_cache.keys() {
let price = mango_client
.get_oracle_price(
market_name
.split('/')
.collect::<Vec<&str>>()
.get(0)
.unwrap(),
)
.unwrap();
price_arcs.insert(
market_name.to_owned(),
Arc::new(RwLock::new(
I80F48::from_num(price.price) / I80F48::from_num(10u64.pow(-price.expo as u32)),
)),
);
}
let handles1 = mango_client
.serum3_markets_cache
.keys()
.map(|market_name| {
loop_blocking_price_update(
mango_client.clone(),
market_name.to_owned(),
price_arcs.get(market_name).unwrap().clone(),
)
})
.collect::<Vec<_>>();
let handles2 = mango_client
.serum3_markets_cache
.keys()
.map(|market_name| {
loop_blocking_orders(
mango_client.clone(),
market_name.to_owned(),
price_arcs.get(market_name).unwrap().clone(),
)
})
.collect::<Vec<_>>();
futures::join!(
futures::future::join_all(handles1),
futures::future::join_all(handles2)
);
Ok(())
}
fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
let account = mango_client.get_account()?.1;
for (_, serum3_market) in mango_client.serum3_markets_cache.values() {
if account.serum3.find(serum3_market.market_index).is_none() {
mango_client.serum3_create_open_orders(serum3_market.name())?;
}
}
Ok(())
}
fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
let mango_account = mango_client.get_account()?.1;
for (_, bank) in mango_client.banks_cache.values() {
let mint = &mango_client.mint_infos_cache.get(&bank.mint).unwrap().2;
let desired_balance = I80F48::from_num(10_000 * 10u64.pow(mint.decimals as u32));
let token_account_opt = mango_account.tokens.find(bank.token_index);
let deposit_native = match token_account_opt {
Some(token_account) => {
let native = token_account.native(bank);
let ui = token_account.ui(bank, mint);
log::info!("Current balance {} {}", ui, bank.name());
if native < I80F48::ZERO {
desired_balance - native
} else {
desired_balance - native.min(desired_balance)
}
}
None => desired_balance,
};
if deposit_native == I80F48::ZERO {
continue;
}
log::info!("Depositing {} {}", deposit_native, bank.name());
mango_client.deposit(bank.name(), desired_balance.to_num())?;
}
Ok(())
}
pub async fn loop_blocking_price_update(
mango_client: Arc<MangoClient>,
market_name: String,
price: Arc<RwLock<I80F48>>,
) {
let mut interval = time::interval(Duration::from_secs(1));
loop {
interval.tick().await;
let client1 = mango_client.clone();
let market_name1 = market_name.clone();
let price = price.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let token_name = market_name1.split('/').collect::<Vec<&str>>()[0];
let fresh_price = client1.get_oracle_price(token_name).unwrap();
log::info!("{} Updated price is {:?}", token_name, fresh_price.price);
if let Ok(mut price) = price.write() {
*price = I80F48::from_num(fresh_price.price)
/ I80F48::from_num(10u64.pow(-fresh_price.expo as u32));
}
Ok(())
})
.await;
match res {
Ok(inner_res) => {
if inner_res.is_err() {
log::error!("{}", inner_res.unwrap_err());
}
}
Err(join_error) => {
log::error!("{}", join_error);
}
}
}
}
pub async fn loop_blocking_orders(
mango_client: Arc<MangoClient>,
market_name: String,
price: Arc<RwLock<I80F48>>,
) {
let mut interval = time::interval(Duration::from_secs(5));
// Cancel existing orders
let orders: Vec<u128> = mango_client.serum3_cancel_all_orders(&market_name).unwrap();
log::info!("Cancelled orders - {:?} for {}", orders, market_name);
loop {
interval.tick().await;
let client = mango_client.clone();
let market_name = market_name.clone();
let price = price.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
client.serum3_settle_funds(&market_name)?;
let fresh_price = match price.read() {
Ok(price) => *price,
Err(_) => {
anyhow::bail!("Price RwLock PoisonError!");
}
};
let fresh_price = fresh_price.to_num::<f64>();
let bid_price = fresh_price + fresh_price * 0.1;
let res = client.serum3_place_order(
&market_name,
Serum3Side::Bid,
bid_price,
0.0001,
Serum3SelfTradeBehavior::DecrementTake,
Serum3OrderType::ImmediateOrCancel,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
10,
);
if let Err(e) = res {
log::error!("Error while placing taker bid {:#?}", e)
} else {
log::info!("Placed bid at {} for {}", bid_price, market_name)
}
let ask_price = fresh_price - fresh_price * 0.1;
let res = client.serum3_place_order(
&market_name,
Serum3Side::Ask,
ask_price,
0.0001,
Serum3SelfTradeBehavior::DecrementTake,
Serum3OrderType::ImmediateOrCancel,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
10,
);
if let Err(e) = res {
log::error!("Error while placing taker ask {:#?}", e)
} else {
log::info!("Placed ask at {} for {}", ask_price, market_name)
}
Ok(())
})
.await;
match res {
Ok(inner_res) => {
if inner_res.is_err() {
log::error!("{}", inner_res.unwrap_err());
}
}
Err(join_error) => {
log::error!("{}", join_error);
}
}
}
}

View File

@ -1,57 +0,0 @@
use std::{sync::Arc, time::Duration};
use mango_v4::state::PerpMarket;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
use tokio::time;
use crate::MangoClient;
pub async fn loop_blocking(mango_client: Arc<MangoClient>, pk: Pubkey, perp_market: PerpMarket) {
let mut interval = time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
let client = mango_client.clone();
tokio::task::spawn_blocking(move || {
perform_operation(client, pk, perp_market).expect("Something went wrong here...");
})
.await
.expect("Something went wrong here...");
}
}
pub fn perform_operation(
mango_client: Arc<MangoClient>,
pk: Pubkey,
perp_market: PerpMarket,
) -> anyhow::Result<()> {
let sig_result = mango_client
.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpUpdateFunding {
perp_market: pk,
asks: perp_market.asks,
bids: perp_market.bids,
oracle: perp_market.oracle,
},
None,
),
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpUpdateFunding {}),
})
.send();
match sig_result {
Ok(sig) => {
log::info!(
"Crank: update funding for perp_market {:?} ix signature: {:?}",
format!("{: >6}", perp_market.name()),
sig
);
}
Err(e) => log::error!("Crank: {:?}", e),
}
Ok(())
}

View File

@ -1,52 +0,0 @@
use std::{sync::Arc, time::Duration};
use mango_v4::state::Bank;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
use tokio::time;
use crate::MangoClient;
pub async fn loop_blocking(mango_client: Arc<MangoClient>, pk: Pubkey, bank: Bank) {
let mut interval = time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
let client = mango_client.clone();
tokio::task::spawn_blocking(move || {
perform_operation(client, pk, bank).expect("Something went wrong here...");
})
.await
.expect("Something went wrong here...");
}
}
pub fn perform_operation(
mango_client: Arc<MangoClient>,
pk: Pubkey,
bank: Bank,
) -> anyhow::Result<()> {
let sig_result = mango_client
.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::UpdateIndex { bank: pk },
None,
),
data: anchor_lang::InstructionData::data(&mango_v4::instruction::UpdateIndex {}),
})
.send();
match sig_result {
Ok(sig) => {
log::info!(
"Crank: update_index for bank {:?} ix signature: {:?}",
bank.name(),
sig
);
}
Err(e) => log::error!("Crank: {:?}", e),
}
Ok(())
}

27
keeper/src/util.rs Normal file
View File

@ -0,0 +1,27 @@
use anyhow::anyhow;
use solana_sdk::signature::Keypair;
#[allow(dead_code)]
pub fn retry<T>(request: impl Fn() -> Result<T, anchor_client::ClientError>) -> anyhow::Result<T> {
for _i in 0..5 {
match request() {
Ok(res) => return Ok(res),
Err(err) => {
// TODO: only retry for recoverable errors
log::error!("{:#?}", err);
continue;
}
}
}
Err(anyhow!("Retry failed"))
}
pub trait MyClone {
fn clone(&self) -> Self;
}
impl MyClone for Keypair {
fn clone(&self) -> Keypair {
Self::from_bytes(&self.to_bytes()).unwrap()
}
}

View File

@ -33,15 +33,9 @@ pub fn create_account(ctx: Context<CreateAccount>, account_num: u8, name: String
name: fill32_from_str(name)?,
group: ctx.accounts.group.key(),
owner: ctx.accounts.owner.key(),
delegate: Pubkey::default(),
tokens: MangoAccountTokens::new(),
serum3: MangoAccountSerum3::new(),
perps: MangoAccountPerps::new(),
being_liquidated: 0,
is_bankrupt: 0,
account_num,
bump: *ctx.bumps.get("account").ok_or(MangoError::SomeError)?,
reserved: Default::default(),
..MangoAccount::default()
};
Ok(())

View File

@ -4,10 +4,11 @@ use crate::error::*;
use crate::state::*;
#[derive(Accounts)]
#[instruction(group_num: u32)]
pub struct CreateGroup<'info> {
#[account(
init,
seeds = [b"Group".as_ref(), admin.key().as_ref()],
seeds = [b"Group".as_ref(), admin.key().as_ref(), &group_num.to_le_bytes()],
bump,
payer = payer,
space = 8 + std::mem::size_of::<Group>(),
@ -22,9 +23,10 @@ pub struct CreateGroup<'info> {
pub system_program: Program<'info, System>,
}
pub fn create_group(ctx: Context<CreateGroup>) -> Result<()> {
pub fn create_group(ctx: Context<CreateGroup>, group_num: u32) -> Result<()> {
let mut group = ctx.accounts.group.load_init()?;
group.admin = ctx.accounts.admin.key();
group.bump = *ctx.bumps.get("group").ok_or(MangoError::SomeError)?;
group.group_num = group_num;
Ok(())
}

View File

@ -2,7 +2,8 @@ use anchor_lang::prelude::*;
use crate::error::*;
use crate::state::{
oracle_price, Book, EventQueue, Group, MangoAccount, OrderType, PerpMarket, Side,
compute_health_from_fixed_accounts, oracle_price, Book, EventQueue, Group, HealthType,
MangoAccount, OrderType, PerpMarket, Side,
};
#[derive(Accounts)]
@ -76,54 +77,62 @@ pub fn perp_place_order(
// When the limit is reached, processing stops and the instruction succeeds.
limit: u8,
) -> Result<()> {
// TODO: check pre and post health
let mut mango_account = ctx.accounts.account.load_mut()?;
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
let mango_account_pk = ctx.accounts.account.key();
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = &ctx.accounts.bids.to_account_info();
let asks = &ctx.accounts.asks.to_account_info();
let mut book = Book::load_mut(bids, asks, &perp_market)?;
{
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = &ctx.accounts.bids.to_account_info();
let asks = &ctx.accounts.asks.to_account_info();
let mut book = Book::load_mut(bids, asks, &perp_market)?;
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
let oracle_price = oracle_price(&ctx.accounts.oracle.to_account_info())?;
let oracle_price = oracle_price(&ctx.accounts.oracle.to_account_info())?;
let now_ts = Clock::get()?.unix_timestamp as u64;
let time_in_force = if expiry_timestamp != 0 {
// If expiry is far in the future, clamp to 255 seconds
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
if tif == 0 {
// If expiry is in the past, ignore the order
msg!("Order is already expired");
return Ok(());
}
tif as u8
} else {
// Never expire
0
};
let now_ts = Clock::get()?.unix_timestamp as u64;
let time_in_force = if expiry_timestamp != 0 {
// If expiry is far in the future, clamp to 255 seconds
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
if tif == 0 {
// If expiry is in the past, ignore the order
msg!("Order is already expired");
return Ok(());
}
tif as u8
} else {
// Never expire
0
};
// TODO reduce_only based on event queue
// TODO reduce_only based on event queue
book.new_order(
side,
&mut perp_market,
&mut event_queue,
oracle_price,
&mut mango_account.perps,
&mango_account_pk,
price_lots,
max_base_lots,
max_quote_lots,
order_type,
time_in_force,
client_order_id,
now_ts,
limit,
book.new_order(
side,
&mut perp_market,
&mut event_queue,
oracle_price,
&mut mango_account.perps,
&mango_account_pk,
price_lots,
max_base_lots,
max_quote_lots,
order_type,
time_in_force,
client_order_id,
now_ts,
limit,
)?;
}
let health = compute_health_from_fixed_accounts(
&mango_account,
HealthType::Init,
ctx.remaining_accounts,
)?;
msg!("health: {}", health);
require!(health >= 0, MangoError::HealthMustBePositive);
Ok(())
}

View File

@ -131,6 +131,7 @@ pub fn register_token(
token_index,
bump: *ctx.bumps.get("bank").ok_or(MangoError::SomeError)?,
reserved: Default::default(),
mint_decimals: ctx.accounts.mint.decimals,
};
// TODO: ALTs are unavailable

View File

@ -25,8 +25,8 @@ pub mod mango_v4 {
use super::*;
pub fn create_group(ctx: Context<CreateGroup>) -> Result<()> {
instructions::create_group(ctx)
pub fn create_group(ctx: Context<CreateGroup>, group_num: u32) -> Result<()> {
instructions::create_group(ctx, group_num)
}
#[allow(clippy::too_many_arguments)]

View File

@ -61,7 +61,9 @@ pub struct Bank {
pub bump: u8,
pub reserved: [u8; 5],
pub mint_decimals: u8,
pub reserved: [u8; 4],
}
const_assert_eq!(size_of::<Bank>(), 16 + 32 * 4 + 8 + 16 * 18 + 3 + 5);
const_assert_eq!(size_of::<Bank>() % 8, 0);

View File

@ -5,7 +5,6 @@ use std::mem::size_of;
// TODO: Assuming we allow up to 65536 different tokens
pub type TokenIndex = u16;
// TODO: Should we call this `Group` instead of `Group`? And `Account` instead of `MangoAccount`?
#[account(zero_copy)]
pub struct Group {
// Relying on Anchor's discriminator be sufficient for our versioning needs?
@ -13,15 +12,22 @@ pub struct Group {
pub admin: Pubkey,
pub bump: u8,
pub reserved: [u8; 7],
pub padding: [u8; 3],
pub group_num: u32,
pub reserved: [u8; 8],
}
const_assert_eq!(size_of::<Group>(), 40);
const_assert_eq!(size_of::<Group>(), 48);
const_assert_eq!(size_of::<Group>() % 8, 0);
#[macro_export]
macro_rules! group_seeds {
( $group:expr ) => {
&[b"Group".as_ref(), $group.admin.as_ref(), &[$group.bump]]
&[
b"Group".as_ref(),
$group.admin.as_ref(),
&$group.group_num.to_le_bytes(),
&[$group.bump],
]
};
}

View File

@ -2,7 +2,6 @@ use anchor_lang::prelude::*;
use fixed::types::I80F48;
use serum_dex::state::OpenOrders;
use std::cell::{Ref, RefMut};
use std::cmp::min;
use std::collections::HashMap;
use crate::error::MangoError;
@ -124,7 +123,7 @@ impl<'a, 'b> ScanningAccountRetriever<'a, 'b> {
};
}
// skip all banks and oracles
// skip all banks and oracles, then find number of PerpMarket accounts
let skip = token_index_map.len() * 2;
let mut perp_index_map = HashMap::with_capacity(ais.len() - skip);
for (i, ai) in ais[skip..].iter().enumerate() {
@ -135,7 +134,9 @@ impl<'a, 'b> ScanningAccountRetriever<'a, 'b> {
}
Err(Error::AnchorError(error))
if error.error_code_number
== ErrorCode::AccountDiscriminatorMismatch as u32 =>
== ErrorCode::AccountDiscriminatorMismatch as u32
|| error.error_code_number
== ErrorCode::AccountOwnedByWrongProgram as u32 =>
{
break;
}
@ -214,7 +215,7 @@ impl<'a, 'b> AccountRetriever<'a, 'b> for ScanningAccountRetriever<'a, 'b> {
let oo = self.ais[self.begin_serum3()..]
.iter()
.find(|ai| ai.key == key)
.unwrap();
.ok_or_else(|| error!(MangoError::SomeError))?;
serum3_cpi::load_open_orders(oo)
}
}
@ -243,15 +244,16 @@ pub fn compute_health_from_fixed_accounts(
let active_serum3_len = account.serum3.iter_active().count();
let active_perp_len = account.perps.iter_active_accounts().count();
let expected_ais = cm!(active_token_len * 2 // banks + oracles
+ active_serum3_len // open_orders
+ active_perp_len); // PerpMarkets
+ active_perp_len // PerpMarkets
+ active_serum3_len); // open_orders
msg!("{} {}", ais.len(), expected_ais);
require!(ais.len() == expected_ais, MangoError::SomeError);
let retriever = FixedOrderAccountRetriever {
ais,
n_banks: active_token_len,
begin_serum3: cm!(active_token_len * 2),
begin_perp: cm!(active_token_len * 2 + active_serum3_len),
begin_perp: cm!(active_token_len * 2),
begin_serum3: cm!(active_token_len * 2 + active_perp_len),
};
compute_health_detail(account, &retriever, health_type, true)?.health(health_type)
}
@ -308,8 +310,33 @@ impl TokenInfo {
}
}
struct PerpInfo {
maint_asset_weight: I80F48,
init_asset_weight: I80F48,
maint_liab_weight: I80F48,
init_liab_weight: I80F48,
// in health-reference-token native units, needs scaling by asset/liab
base: I80F48,
// in health-reference-token native units, no asset/liab factor needed
quote: I80F48,
}
impl PerpInfo {
#[inline(always)]
fn health_contribution(&self, health_type: HealthType) -> I80F48 {
let factor = match (health_type, self.base.is_negative()) {
(HealthType::Init, true) => self.init_liab_weight,
(HealthType::Init, false) => self.init_asset_weight,
(HealthType::Maint, true) => self.maint_liab_weight,
(HealthType::Maint, false) => self.maint_asset_weight,
};
cm!(self.quote + factor * self.base)
}
}
pub struct HealthCache {
token_infos: Vec<TokenInfo>,
perp_infos: Vec<PerpInfo>,
}
impl HealthCache {
@ -319,6 +346,10 @@ impl HealthCache {
let contrib = health_contribution(health_type, token_info, token_info.balance)?;
health = cm!(health + contrib);
}
for perp_info in self.perp_infos.iter() {
let contrib = perp_info.health_contribution(health_type);
health = cm!(health + contrib);
}
Ok(health)
}
@ -350,28 +381,6 @@ fn health_contribution(
Ok(cm!(balance * weight))
}
/// Weigh a perp base balance (in lots) with the appropriate health weight
#[inline(always)]
fn health_weighted_perp_base_lots(
health_type: HealthType,
market: &PerpMarket,
lots: i64,
) -> Result<I80F48> {
let weight = if lots.is_negative() {
match health_type {
HealthType::Init => market.init_liab_weight,
HealthType::Maint => market.maint_liab_weight,
}
} else {
match health_type {
HealthType::Init => market.init_asset_weight,
HealthType::Maint => market.maint_asset_weight,
}
};
let lots = I80F48::from(lots);
Ok(cm!(weight * lots))
}
/// Compute health contribution of two tokens - pure convenience
#[inline(always)]
fn pair_health(
@ -473,6 +482,7 @@ fn compute_health_detail<'a, 'b: 'a>(
}
// health contribution from perp accounts
let mut perp_infos = Vec::with_capacity(account.perps.iter_active_accounts().count());
for (i, perp_account) in account.perps.iter_active_accounts().enumerate() {
let perp_market = retriever.perp_market(&account.group, i, perp_account.market_index)?;
@ -481,17 +491,7 @@ fn compute_health_detail<'a, 'b: 'a>(
.iter()
.position(|ti| ti.token_index == perp_market.base_token_index)
.ok_or_else(|| error!(MangoError::SomeError))?;
let quote_index = token_infos
.iter()
.position(|ti| ti.token_index == perp_market.quote_token_index)
.ok_or_else(|| error!(MangoError::SomeError))?;
let (base_info, quote_info) = if base_index < quote_index {
let (l, r) = token_infos.split_at_mut(quote_index);
(&mut l[base_index], &mut r[0])
} else {
let (l, r) = token_infos.split_at_mut(base_index);
(&mut r[0], &mut l[quote_index])
};
let base_info = &token_infos[base_index];
let base_lot_size = I80F48::from(perp_market.base_lot_size);
@ -499,88 +499,92 @@ fn compute_health_detail<'a, 'b: 'a>(
let taker_quote = I80F48::from(cm!(
perp_account.taker_quote_lots * perp_market.quote_lot_size
));
let quote = cm!(perp_account.quote_position_native + taker_quote);
let quote_current = cm!(perp_account.quote_position_native + taker_quote);
// Two scenarios:
// 1. The price goes low and all bids execute, converting to base.
// That means the perp position is increased by `bids` and the quote position
// is decreased by `bids * base_lot_size * price`.
// The health for this case is:
// (weighted(base_lots + bids) - bids) * base_lots * price + quote
// (weighted(base_lots + bids) - bids) * base_lot_size * price + quote
// 2. The price goes high and all asks execute, converting to quote.
// The health for this case is:
// (weighted(base_lots - asks) + asks) * base_lots * price + quote
// (weighted(base_lots - asks) + asks) * base_lot_size * price + quote
//
// Comparing these makes it clear we need to pick the worse subfactor
// weighted(base_lots + bids) - bids
// weighted(base_lots + bids) - bids =: scenario1
// or
// weighted(base_lots - asks) + asks
let weighted_base_lots_bids = health_weighted_perp_base_lots(
health_type,
&perp_market,
cm!(base_lots + perp_account.bids_base_lots),
)?;
let bids_base_lots = I80F48::from(perp_account.bids_base_lots);
let scenario1 = cm!(weighted_base_lots_bids - bids_base_lots);
// weighted(base_lots - asks) + asks =: scenario2
//
// Additionally, we want this scenario choice to be the same no matter whether we're
// computing init or maint health. This can be guaranteed by requiring the weights
// to satisfy the property (P):
//
// (1 - init_asset_weight) / (init_liab_weight - 1)
// == (1 - maint_asset_weight) / (maint_liab_weight - 1)
//
// Derivation:
// Set asks_net_lots := base_lots - asks, bids_net_lots := base_lots + bids.
// Now
// scenario1 = weighted(bids_net_lots) - bids_net_lots + base_lots and
// scenario2 = weighted(asks_net_lots) - asks_net_lots + base_lots
// So with expanding weigthed(a) = weight_factor_for_a * a, the question
// scenario1 < scenario2
// becomes:
// (weight_factor_for_bids_net_lots - 1) * bids_net_lots
// < (weight_factor_for_asks_net_lots - 1) * asks_net_lots
// Since asks_net_lots < 0 and bids_net_lots > 0 is the only interesting case, (P) follows.
//
// We satisfy (P) by requiring
// asset_weight = 1 - x and liab_weight = 1 + x
//
// And with that assumption the scenario choice condition further simplifies to:
// scenario1 < scenario2
// iff abs(bids_net_lots) > abs(asks_net_lots)
let bids_net_lots = cm!(base_lots + perp_account.bids_base_lots);
let asks_net_lots = cm!(base_lots - perp_account.asks_base_lots);
let weighted_base_lots_asks = health_weighted_perp_base_lots(
health_type,
&perp_market,
cm!(base_lots - perp_account.asks_base_lots),
)?;
let asks_base_lots = I80F48::from(perp_account.asks_base_lots);
let scenario2 = cm!(weighted_base_lots_asks + asks_base_lots);
let lots_to_quote = base_lot_size * base_info.oracle_price;
let base;
let quote;
if cm!(bids_net_lots.abs()) > cm!(asks_net_lots.abs()) {
let bids_net_lots = I80F48::from(bids_net_lots);
let bids_base_lots = I80F48::from(perp_account.bids_base_lots);
base = cm!(bids_net_lots * lots_to_quote);
quote = cm!(quote_current - bids_base_lots * lots_to_quote);
} else {
let asks_net_lots = I80F48::from(asks_net_lots);
let asks_base_lots = I80F48::from(perp_account.asks_base_lots);
base = cm!(asks_net_lots * lots_to_quote);
quote = cm!(quote_current + asks_base_lots * lots_to_quote);
};
// TODO remove since unused
{
let worse_scenario = min(scenario1, scenario2);
let _health = cm!(worse_scenario * base_lot_size * base_info.oracle_price + quote);
// The above choice between scenario1 and 2 depends on the asset_weight and
// liab weight. Thus it needs to be redone for init and maint health.
//
// The condition for the choice to be the same is:
// (1 - init_asset_weight) / (init_liab_weight - 1)
// == (1 - maint_asset_weight) / (maint_liab_weight - 1)
//
// Which can be derived by noticing that health for both scenarios is
// weighted(x) + y - x
// and that the only interesting case is
// asks_net = base_lots - asks < 0 and
// bids_net = base_lots + bids > 0.
// Then
// health_bids_scenario < health_asks_scenario
// iff (asset_weight - 1) * bids_net < (liab_weight - 1) * asks_net
// iff (1 - asset_weightt) / (liab_weight - 1) bids_net > abs(asks_net)
// Probably the resolution here is to go to v3's assumption that there's an x
// such that asset_weight = 1-x and liab_weight = 1+x.
// This is ok as long as perp markets are strictly isolated.
}
// if all bids were executed
if scenario1 <= scenario2 {
base_info.balance = base_info.balance + I80F48::from(base_lots) + bids_base_lots;
quote_info.balance = quote_info.balance
+ -bids_base_lots * base_lot_size * base_info.oracle_price
+ quote;
}
// if all asks were executed
else {
base_info.balance = base_info.balance + I80F48::from(base_lots) - asks_base_lots;
quote_info.balance = quote_info.balance
+ asks_base_lots * base_lot_size * base_info.oracle_price
+ quote;
}
perp_infos.push(PerpInfo {
init_asset_weight: perp_market.init_asset_weight,
init_liab_weight: perp_market.init_liab_weight,
maint_asset_weight: perp_market.maint_asset_weight,
maint_liab_weight: perp_market.maint_liab_weight,
base,
quote,
});
}
Ok(HealthCache { token_infos })
Ok(HealthCache {
token_infos,
perp_infos,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::oracle::StubOracle;
use std::cell::RefCell;
use std::convert::identity;
use std::mem::size_of;
use std::rc::Rc;
use std::str::FromStr;
use fixed::types::I80F48;
#[test]
fn test_precision() {
// I80F48 can only represent until 1/2^48
@ -603,4 +607,344 @@ mod tests {
0
);
}
// Implementing TestAccount directly for ZeroCopy + Owner leads to a conflict
// because OpenOrders may add impls for those in the future.
trait MyZeroCopy: anchor_lang::ZeroCopy + Owner {}
impl MyZeroCopy for StubOracle {}
impl MyZeroCopy for Bank {}
impl MyZeroCopy for PerpMarket {}
struct TestAccount<T> {
bytes: Vec<u8>,
pubkey: Pubkey,
owner: Pubkey,
lamports: u64,
_phantom: std::marker::PhantomData<T>,
}
impl<T> TestAccount<T> {
fn new(bytes: Vec<u8>, owner: Pubkey) -> Self {
Self {
bytes,
owner,
pubkey: Pubkey::new_unique(),
lamports: 0,
_phantom: std::marker::PhantomData,
}
}
fn as_account_info(&mut self) -> AccountInfo {
AccountInfo {
key: &self.pubkey,
owner: &self.owner,
lamports: Rc::new(RefCell::new(&mut self.lamports)),
data: Rc::new(RefCell::new(&mut self.bytes)),
is_signer: false,
is_writable: false,
executable: false,
rent_epoch: 0,
}
}
}
impl<T: MyZeroCopy> TestAccount<T> {
fn new_zeroed() -> Self {
let mut bytes = vec![0u8; 8 + size_of::<T>()];
bytes[0..8].copy_from_slice(&T::discriminator());
Self::new(bytes, T::owner())
}
fn data(&mut self) -> &mut T {
bytemuck::from_bytes_mut(&mut self.bytes[8..])
}
}
impl TestAccount<OpenOrders> {
fn new_zeroed() -> Self {
let mut bytes = vec![0u8; 12 + size_of::<OpenOrders>()];
bytes[0..5].copy_from_slice(b"serum");
Self::new(bytes, Pubkey::new_unique())
}
fn data(&mut self) -> &mut OpenOrders {
bytemuck::from_bytes_mut(&mut self.bytes[5..5 + size_of::<OpenOrders>()])
}
}
fn mock_bank_and_oracle(
group: Pubkey,
token_index: TokenIndex,
price: f64,
init_weights: f64,
maint_weights: f64,
) -> (TestAccount<Bank>, TestAccount<StubOracle>) {
let mut oracle = TestAccount::<StubOracle>::new_zeroed();
oracle.data().price = I80F48::from_num(price);
let mut bank = TestAccount::<Bank>::new_zeroed();
bank.data().token_index = token_index;
bank.data().group = group;
bank.data().oracle = oracle.pubkey;
bank.data().deposit_index = I80F48::from(1_000_000);
bank.data().borrow_index = I80F48::from(1_000_000);
bank.data().init_asset_weight = I80F48::from_num(1.0 - init_weights);
bank.data().init_liab_weight = I80F48::from_num(1.0 + init_weights);
bank.data().maint_asset_weight = I80F48::from_num(1.0 - maint_weights);
bank.data().maint_liab_weight = I80F48::from_num(1.0 + maint_weights);
(bank, oracle)
}
// Run a health test that includes all the side values (like referrer_rebates_accrued)
#[test]
fn test_health0() {
let mut account = MangoAccount::default();
let group = Pubkey::new_unique();
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
bank1
.data()
.deposit(
account.tokens.get_mut_or_create(1).unwrap().0,
I80F48::from(100),
)
.unwrap();
bank2
.data()
.withdraw_without_fee(
account.tokens.get_mut_or_create(4).unwrap().0,
I80F48::from(10),
)
.unwrap();
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
let serum3account = account.serum3.create(2).unwrap();
serum3account.open_orders = oo1.pubkey;
serum3account.base_token_index = 4;
serum3account.quote_token_index = 1;
oo1.data().native_pc_total = 21;
oo1.data().native_coin_total = 18;
oo1.data().native_pc_free = 1;
oo1.data().native_coin_free = 3;
oo1.data().referrer_rebates_accrued = 2;
let mut perp1 = TestAccount::<PerpMarket>::new_zeroed();
perp1.data().group = group;
perp1.data().perp_market_index = 9;
perp1.data().base_token_index = 4;
perp1.data().quote_token_index = 1;
perp1.data().init_asset_weight = I80F48::from_num(1.0 - 0.2f64);
perp1.data().init_liab_weight = I80F48::from_num(1.0 + 0.2f64);
perp1.data().maint_asset_weight = I80F48::from_num(1.0 - 0.1f64);
perp1.data().maint_liab_weight = I80F48::from_num(1.0 + 0.1f64);
perp1.data().quote_lot_size = 100;
perp1.data().base_lot_size = 10;
let perpaccount = account.perps.get_account_mut_or_create(9).unwrap().0;
perpaccount.base_position_lots = 3;
perpaccount.quote_position_native = I80F48::from(31u8);
perpaccount.bids_base_lots = 7;
perpaccount.asks_base_lots = 11;
perpaccount.taker_base_lots = 1;
perpaccount.taker_quote_lots = 2;
let ais = vec![
bank1.as_account_info(),
bank2.as_account_info(),
oracle1.as_account_info(),
oracle2.as_account_info(),
perp1.as_account_info(),
oo1.as_account_info(),
];
let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap();
let health_eq = |a: I80F48, b: f64| {
if (a - I80F48::from_num(b)).abs() < 0.001 {
true
} else {
println!("health is {}, but expected {}", a, b);
false
}
};
// for bank1/oracle1, including open orders (scenario: bids execute)
let health1 = (100.0 + 1.0 + 2.0 + (20.0 + 15.0 * 5.0)) * 0.8;
// for bank2/oracle2
let health2 = (-10.0 + 3.0) * 5.0 * 1.5;
// for perp (scenario: bids execute)
let health3 =
(3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 + (31.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0);
assert!(health_eq(
compute_health(&account, HealthType::Init, &retriever).unwrap(),
health1 + health2 + health3
));
}
#[test]
fn test_scanning_account_retriever() {
let group = Pubkey::new_unique();
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
let oo1key = oo1.pubkey;
oo1.data().native_pc_total = 20;
let mut perp1 = TestAccount::<PerpMarket>::new_zeroed();
perp1.data().group = group;
perp1.data().perp_market_index = 9;
let ais = vec![
bank1.as_account_info(),
bank2.as_account_info(),
oracle1.as_account_info(),
oracle2.as_account_info(),
perp1.as_account_info(),
oo1.as_account_info(),
];
let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap();
assert_eq!(retriever.n_banks(), 2);
assert_eq!(retriever.begin_serum3(), 5);
assert_eq!(retriever.perp_index_map.len(), 1);
let (b1, o1) = retriever.bank_mut_and_oracle(1).unwrap();
assert_eq!(b1.token_index, 1);
assert_eq!(o1.key, ais[2].key);
let (b2, o2) = retriever.bank_mut_and_oracle(4).unwrap();
assert_eq!(b2.token_index, 4);
assert_eq!(o2.key, ais[3].key);
retriever.bank_mut_and_oracle(2).unwrap_err();
let oo = retriever.serum_oo(0, &oo1key).unwrap();
assert_eq!(identity(oo.native_pc_total), 20);
assert!(retriever.serum_oo(1, &Pubkey::default()).is_err());
let perp = retriever.perp_market(&group, 0, 9).unwrap();
assert_eq!(identity(perp.perp_market_index), 9);
assert!(retriever.perp_market(&group, 1, 5).is_err());
}
struct TestHealth1Case {
token1: i64,
token2: i64,
oo_1_2: (u64, u64),
perp1: (i64, i64, i64, i64),
expected_health: f64,
}
fn test_health1_runner(testcase: &TestHealth1Case) {
let mut account = MangoAccount::default();
let group = Pubkey::new_unique();
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
bank1
.data()
.change_without_fee(
account.tokens.get_mut_or_create(1).unwrap().0,
I80F48::from(testcase.token1),
)
.unwrap();
bank2
.data()
.change_without_fee(
account.tokens.get_mut_or_create(4).unwrap().0,
I80F48::from(testcase.token2),
)
.unwrap();
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
let serum3account = account.serum3.create(2).unwrap();
serum3account.open_orders = oo1.pubkey;
serum3account.base_token_index = 4;
serum3account.quote_token_index = 1;
oo1.data().native_pc_total = testcase.oo_1_2.0;
oo1.data().native_coin_total = testcase.oo_1_2.1;
let mut perp1 = TestAccount::<PerpMarket>::new_zeroed();
perp1.data().group = group;
perp1.data().perp_market_index = 9;
perp1.data().base_token_index = 4;
perp1.data().quote_token_index = 1;
perp1.data().init_asset_weight = I80F48::from_num(1.0 - 0.2f64);
perp1.data().init_liab_weight = I80F48::from_num(1.0 + 0.2f64);
perp1.data().maint_asset_weight = I80F48::from_num(1.0 - 0.1f64);
perp1.data().maint_liab_weight = I80F48::from_num(1.0 + 0.1f64);
perp1.data().quote_lot_size = 100;
perp1.data().base_lot_size = 10;
let perpaccount = account.perps.get_account_mut_or_create(9).unwrap().0;
perpaccount.base_position_lots = testcase.perp1.0;
perpaccount.quote_position_native = I80F48::from(testcase.perp1.1);
perpaccount.bids_base_lots = testcase.perp1.2;
perpaccount.asks_base_lots = testcase.perp1.3;
let ais = vec![
bank1.as_account_info(),
bank2.as_account_info(),
oracle1.as_account_info(),
oracle2.as_account_info(),
perp1.as_account_info(),
oo1.as_account_info(),
];
let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap();
let health_eq = |a: I80F48, b: f64| {
if (a - I80F48::from_num(b)).abs() < 0.001 {
true
} else {
println!("health is {}, but expected {}", a, b);
false
}
};
assert!(health_eq(
compute_health(&account, HealthType::Init, &retriever).unwrap(),
testcase.expected_health
));
}
// Check some specific health constellations
#[test]
fn test_health1() {
let testcases = vec![
TestHealth1Case {
token1: 100,
token2: -10,
oo_1_2: (20, 15),
perp1: (3, 31, 7, 11),
expected_health:
// for token1, including open orders (scenario: bids execute)
(100.0 + (20.0 + 15.0 * 5.0)) * 0.8
// for token2
- 10.0 * 5.0 * 1.5
// for perp (scenario: bids execute)
+ (3.0 + 7.0) * 10.0 * 5.0 * 0.8 + (31.0 - 7.0 * 10.0 * 5.0),
},
TestHealth1Case {
token1: -100,
token2: 10,
oo_1_2: (20, 15),
perp1: (-10, 31, 7, 11),
expected_health:
// for token1
-100.0 * 1.2
// for token2, including open orders (scenario: asks execute)
+ (10.0 * 5.0 + (20.0 + 15.0 * 5.0)) * 0.5
// for perp (scenario: asks execute)
+ (-10.0 - 11.0) * 10.0 * 5.0 * 1.2 + (31.0 + 11.0 * 10.0 * 5.0),
},
];
for (i, testcase) in testcases.iter().enumerate() {
println!("checking testcase {}", i);
test_health1_runner(&testcase);
}
}
}

View File

@ -1,4 +1,5 @@
use anchor_lang::prelude::*;
use anchor_spl::token::Mint;
use checked_math as cm;
use fixed::types::I80F48;
use static_assertions::const_assert_eq;
@ -62,6 +63,16 @@ impl TokenAccount {
}
}
pub fn ui(&self, bank: &Bank, mint: &Mint) -> I80F48 {
if self.indexed_value.is_positive() {
(self.indexed_value * bank.deposit_index)
/ I80F48::from_num(10u64.pow(mint.decimals as u32))
} else {
(self.indexed_value * bank.borrow_index)
/ I80F48::from_num(10u64.pow(mint.decimals as u32))
}
}
pub fn is_in_use(&self) -> bool {
self.in_use_count > 0
}
@ -717,13 +728,32 @@ impl std::fmt::Debug for MangoAccount {
}
impl MangoAccount {
fn name(&self) -> &str {
pub fn name(&self) -> &str {
std::str::from_utf8(&self.name)
.unwrap()
.trim_matches(char::from(0))
}
}
impl Default for MangoAccount {
fn default() -> Self {
Self {
name: Default::default(),
group: Pubkey::default(),
owner: Pubkey::default(),
delegate: Pubkey::default(),
tokens: MangoAccountTokens::new(),
serum3: MangoAccountSerum3::new(),
perps: MangoAccountPerps::new(),
being_liquidated: 0,
is_bankrupt: 0,
account_num: 0,
bump: 0,
reserved: Default::default(),
}
}
}
#[macro_export]
macro_rules! account_seeds {
( $account:expr ) => {

View File

@ -7,6 +7,7 @@ use crate::state::*;
pub type Serum3MarketIndex = u16;
#[account(zero_copy)]
#[derive(Debug)]
pub struct Serum3Market {
pub name: [u8; 16],
pub group: Pubkey,

View File

@ -103,17 +103,45 @@ async fn get_mint_info_by_token_index(
get_mint_info_by_mint(account_loader, account, bank.mint).await
}
fn get_perp_market_address_by_index(group: Pubkey, perp_market_index: PerpMarketIndex) -> Pubkey {
Pubkey::find_program_address(
&[
group.as_ref(),
b"PerpMarket".as_ref(),
&perp_market_index.to_le_bytes(),
],
&mango_v4::id(),
)
.0
}
// all the accounts that instructions like deposit/withdraw need to compute account health
async fn derive_health_check_remaining_account_metas(
account_loader: &impl ClientAccountLoader,
account: &MangoAccount,
affected_bank: Option<Pubkey>,
writable_banks: bool,
affected_perp_market_index: Option<PerpMarketIndex>,
) -> Vec<AccountMeta> {
let mut adjusted_account = account.clone();
if let Some(affected_bank) = affected_bank {
let bank: Bank = account_loader.load(&affected_bank).await.unwrap();
adjusted_account
.tokens
.get_mut_or_create(bank.token_index)
.unwrap();
}
if let Some(affected_perp_market_index) = affected_perp_market_index {
adjusted_account
.perps
.get_account_mut_or_create(affected_perp_market_index)
.unwrap();
}
// figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![];
let mut oracles = vec![];
for position in account.tokens.iter_active() {
for position in adjusted_account.tokens.iter_active() {
let mint_info =
get_mint_info_by_token_index(account_loader, account, position.token_index).await;
// TODO: ALTs are unavailable
@ -127,24 +155,20 @@ async fn derive_health_check_remaining_account_metas(
banks.push(mint_info.bank);
oracles.push(mint_info.oracle);
}
if let Some(affected_bank) = affected_bank {
if banks.iter().find(|&&v| v == affected_bank).is_none() {
// If there is not yet an active position for the token, we need to pass
// the bank/oracle for health check anyway.
let new_position = account
.tokens
.values
.iter()
.position(|p| !p.is_active())
.unwrap();
banks.insert(new_position, affected_bank);
let affected_bank: Bank = account_loader.load(&affected_bank).await.unwrap();
oracles.insert(new_position, affected_bank.oracle);
}
}
let perp_markets = adjusted_account
.perps
.iter_active_accounts()
.map(|perp| get_perp_market_address_by_index(account.group, perp.market_index));
let serum_oos = account.serum3.iter_active().map(|&s| s.open_orders);
let to_account_meta = |pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
};
banks
.iter()
.map(|&pubkey| AccountMeta {
@ -152,16 +176,9 @@ async fn derive_health_check_remaining_account_metas(
is_writable: writable_banks,
is_signer: false,
})
.chain(oracles.iter().map(|&pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.chain(serum_oos.map(|pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.chain(oracles.into_iter().map(to_account_meta))
.chain(perp_markets.map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.collect()
}
@ -198,12 +215,25 @@ async fn derive_liquidation_remaining_account_metas(
oracles.push(mint_info.oracle);
}
let perp_markets = liqee
.perps
.iter_active_accounts()
.chain(liqee.perps.iter_active_accounts())
.map(|perp| get_perp_market_address_by_index(liqee.group, perp.market_index))
.unique();
let serum_oos = liqee
.serum3
.iter_active()
.chain(liqor.serum3.iter_active())
.map(|&s| s.open_orders);
let to_account_meta = |pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
};
banks
.iter()
.map(|(pubkey, is_writable)| AccountMeta {
@ -211,16 +241,9 @@ async fn derive_liquidation_remaining_account_metas(
is_writable: *is_writable,
is_signer: false,
})
.chain(oracles.iter().map(|&pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.chain(serum_oos.map(|pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.chain(oracles.into_iter().map(to_account_meta))
.chain(perp_markets.map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.collect()
}
@ -290,6 +313,7 @@ impl<'keypair> ClientInstruction for MarginTradeInstruction<'keypair> {
&account,
Some(self.mango_token_bank),
true,
None,
)
.await;
@ -384,6 +408,7 @@ impl<'keypair> ClientInstruction for WithdrawInstruction<'keypair> {
&account,
Some(mint_info.bank),
false,
None,
)
.await;
@ -447,6 +472,7 @@ impl<'keypair> ClientInstruction for DepositInstruction<'keypair> {
&account,
Some(mint_info.bank),
false,
None,
)
.await;
@ -694,10 +720,14 @@ impl<'keypair> ClientInstruction for CreateGroupInstruction<'keypair> {
_account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let instruction = Self::Instruction { group_num: 0 };
let group = Pubkey::find_program_address(
&[b"Group".as_ref(), self.admin.pubkey().as_ref()],
&[
b"Group".as_ref(),
self.admin.pubkey().as_ref(),
&instruction.group_num.to_le_bytes(),
],
&program_id,
)
.0;
@ -978,9 +1008,14 @@ impl<'keypair> ClientInstruction for Serum3PlaceOrderInstruction<'keypair> {
)
.unwrap();
let health_check_metas =
derive_health_check_remaining_account_metas(&account_loader, &account, None, false)
.await;
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
false,
None,
)
.await;
let accounts = Self::Accounts {
group: account.group,
@ -1205,9 +1240,14 @@ impl ClientInstruction for Serum3LiqForceCancelOrdersInstruction {
)
.unwrap();
let health_check_metas =
derive_health_check_remaining_account_metas(&account_loader, &account, None, false)
.await;
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
false,
None,
)
.await;
let accounts = Self::Accounts {
group: account.group,
@ -1394,7 +1434,7 @@ impl<'keypair> ClientInstruction for PerpPlaceOrderInstruction<'keypair> {
type Instruction = mango_v4::instruction::PerpPlaceOrder;
async fn to_instruction(
&self,
_loader: impl ClientAccountLoader + 'async_trait,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
@ -1418,7 +1458,20 @@ impl<'keypair> ClientInstruction for PerpPlaceOrderInstruction<'keypair> {
owner: self.owner.pubkey(),
};
let instruction = make_instruction(program_id, &accounts, instruction);
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let account: MangoAccount = account_loader.load(&self.account).await.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
false,
Some(perp_market.perp_market_index),
)
.await;
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
(accounts, instruction)
}

View File

@ -1,5 +1,7 @@
#![cfg(feature = "test-bpf")]
use fixed::types::I80F48;
use mango_v4::state::*;
use solana_program_test::*;
use solana_sdk::{signature::Keypair, transport::TransportError};
@ -65,7 +67,7 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
}
// TODO: actual explicit CU comparisons.
// On 2022-3-29 the final deposit costs 43900 CU and each new token increases it by roughly 1800 CU
// On 2022-5-25 the final deposit costs 36905 CU and each new token increases it by roughly 1600 CU
Ok(())
}
@ -166,7 +168,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
DepositInstruction {
amount: 10,
account,
token_account: payer_mint_accounts[i],
token_account: payer_mint_accounts[0],
token_authority: payer,
},
)
@ -175,7 +177,160 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
}
// TODO: actual explicit CU comparisons.
// On 2022-3-29 the final deposit costs 60380 CU and each new market increases it by roughly 5000 CU
// On 2022-5-25 the final deposit costs 52252 CU and each new market increases it by roughly 4400 CU
Ok(())
}
// Try to reach compute limits in health checks by having many perp markets in an account
#[tokio::test]
async fn test_health_compute_perp() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = &Keypair::new();
let owner = &context.users[0].key;
let payer = &context.users[1].key;
let mints = &context.mints[0..8];
let payer_mint_accounts = &context.users[1].token_accounts[0..mints.len()];
//
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
let account = send_tx(
solana,
CreateAccountInstruction {
account_num: 0,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
// Give the account some quote currency
send_tx(
solana,
DepositInstruction {
amount: 1000,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
},
)
.await
.unwrap();
//
// SETUP: Create perp markets
//
let quote_token = &tokens[0];
let mut perp_markets = vec![];
for (perp_market_index, token) in tokens[1..].iter().enumerate() {
let mango_v4::accounts::PerpCreateMarket {
perp_market,
asks,
bids,
event_queue,
..
} = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
oracle: token.oracle,
asks: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
event_queue: {
context
.solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
.await
},
payer,
perp_market_index: perp_market_index as PerpMarketIndex,
base_token_index: quote_token.index,
quote_token_index: token.index,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,
init_asset_weight: 0.95,
maint_liab_weight: 1.025,
init_liab_weight: 1.05,
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
},
)
.await
.unwrap();
perp_markets.push((perp_market, asks, bids, event_queue));
}
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_markets[0].0).await;
perp_market.native_price_to_lot(I80F48::from(1))
};
//
// TEST: Create a perp order for each market
//
for (i, &(perp_market, asks, bids, event_queue)) in perp_markets.iter().enumerate() {
println!("adding market {}", i);
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[i + 1].oracle,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
send_tx(
solana,
DepositInstruction {
amount: 10,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
},
)
.await
.unwrap();
}
// TODO: actual explicit CU comparisons.
// On 2022-5-25 the final deposit costs 32700 CU and each new market increases it by roughly 1500 CU
Ok(())
}

View File

@ -40,6 +40,7 @@ export class Bank {
liquidationFee: I80F48Dto;
dust: Object;
tokenIndex: number;
mintDecimals: number;
},
) {
return new Bank(
@ -69,6 +70,7 @@ export class Bank {
obj.liquidationFee,
obj.dust,
obj.tokenIndex,
obj.mintDecimals,
);
}
@ -99,6 +101,7 @@ export class Bank {
liquidationFee: I80F48Dto,
dust: Object,
public tokenIndex: number,
public mintDecimals: number,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.depositIndex = I80F48.from(depositIndex);

View File

@ -5,13 +5,24 @@ import { PerpMarket } from './perp';
import { Serum3Market } from './serum3';
export class Group {
static from(publicKey: PublicKey, obj: { admin: PublicKey }): Group {
return new Group(publicKey, obj.admin, new Map(), new Map(), new Map());
static from(
publicKey: PublicKey,
obj: { admin: PublicKey; groupNum: number },
): Group {
return new Group(
publicKey,
obj.admin,
obj.groupNum,
new Map(),
new Map(),
new Map(),
);
}
constructor(
public publicKey: PublicKey,
public admin: PublicKey,
public groupNum: number,
public banksMap: Map<string, Bank>,
public serum3MarketsMap: Map<string, Serum3Market>,
public perpMarketsMap: Map<string, PerpMarket>,

View File

@ -51,10 +51,10 @@ export class MangoClient {
// Group
public async createGroup(): Promise<TransactionSignature> {
public async createGroup(groupNum: number): Promise<TransactionSignature> {
const adminPk = (this.program.provider as AnchorProvider).wallet.publicKey;
return await this.program.methods
.createGroup()
.createGroup(groupNum)
.accounts({
admin: adminPk,
payer: adminPk,
@ -69,17 +69,33 @@ export class MangoClient {
return group;
}
public async getGroupForAdmin(adminPk: PublicKey): Promise<Group> {
const groups = (
await this.program.account.group.all([
{
memcmp: {
bytes: adminPk.toBase58(),
offset: 8,
},
public async getGroupForAdmin(
adminPk: PublicKey,
groupNum?: number,
): Promise<Group> {
const filters: MemcmpFilter[] = [
{
memcmp: {
bytes: adminPk.toBase58(),
offset: 8,
},
])
).map((tuple) => Group.from(tuple.publicKey, tuple.account));
},
];
if (groupNum) {
const bbuf = Buffer.alloc(4);
bbuf.writeUInt32LE(groupNum);
filters.push({
memcmp: {
bytes: bs58.encode(bbuf),
offset: 44,
},
});
}
const groups = (await this.program.account.group.all(filters)).map(
(tuple) => Group.from(tuple.publicKey, tuple.account),
);
await groups[0].reload(this);
return groups[0];
}
@ -668,6 +684,7 @@ export class MangoClient {
{ commitment: this.program.provider.connection.commitment },
serum3ProgramId,
);
// TODO: filter for mango account
return await serum3MarketExternal.loadOrdersForOwner(
this.program.provider.connection,
group.publicKey,

View File

@ -20,6 +20,11 @@ export type MangoV4 = {
"kind": "account",
"type": "publicKey",
"path": "admin"
},
{
"kind": "arg",
"type": "u32",
"path": "group_num"
}
]
}
@ -40,7 +45,12 @@ export type MangoV4 = {
"isSigner": false
}
],
"args": []
"args": [
{
"name": "groupNum",
"type": "u32"
}
]
},
{
"name": "registerToken",
@ -1720,12 +1730,16 @@ export type MangoV4 = {
"name": "bump",
"type": "u8"
},
{
"name": "mintDecimals",
"type": "u8"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
5
4
]
}
}
@ -1745,12 +1759,25 @@ export type MangoV4 = {
"name": "bump",
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
3
]
}
},
{
"name": "groupNum",
"type": "u32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
7
8
]
}
}
@ -2893,6 +2920,11 @@ export const IDL: MangoV4 = {
"kind": "account",
"type": "publicKey",
"path": "admin"
},
{
"kind": "arg",
"type": "u32",
"path": "group_num"
}
]
}
@ -2913,7 +2945,12 @@ export const IDL: MangoV4 = {
"isSigner": false
}
],
"args": []
"args": [
{
"name": "groupNum",
"type": "u32"
}
]
},
{
"name": "registerToken",
@ -4593,12 +4630,16 @@ export const IDL: MangoV4 = {
"name": "bump",
"type": "u8"
},
{
"name": "mintDecimals",
"type": "u8"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
5
4
]
}
}
@ -4618,12 +4659,25 @@ export const IDL: MangoV4 = {
"name": "bump",
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
3
]
}
},
{
"name": "groupNum",
"type": "u32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
7
8
]
}
}

View File

@ -47,7 +47,7 @@ async function main() {
// group
console.log(`Creating Group...`);
try {
await client.createGroup();
await client.createGroup(0);
} catch (error) {}
const group = await client.getGroupForAdmin(admin.publicKey);
console.log(`...registered group ${group.publicKey}`);

View File

@ -0,0 +1,70 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../client';
import { DEVNET_SERUM3_PROGRAM_ID } from '../constants';
//
// An example for users based on high level api i.e. the client
// Create
// process.env.USER_KEYPAIR - mango account owner keypair path
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(userProvider, true);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForAdmin(admin.publicKey);
console.log(`Found group ${group.publicKey.toBase58()}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
0,
'my_mango_account',
);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
// logging serum3 open orders for user
while (true) {
console.log(`Current own orders on OB...`);
const orders = await client.getSerum3Orders(
group,
DEVNET_SERUM3_PROGRAM_ID,
'BTC/USDC',
);
for (const order of orders) {
console.log(
` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${
order.size
} ${order.openOrdersAddress.toBase58()}`,
);
}
await new Promise((r) => setTimeout(r, 500));
}
process.exit();
}
main();

View File

@ -0,0 +1,58 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { TokenAccount } from '../accounts/mangoAccount';
import { MangoClient } from '../client';
//
// An example for users based on high level api i.e. the client
// Create
// process.env.USER_KEYPAIR - mango account owner keypair path
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(userProvider, true);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForAdmin(admin.publicKey);
console.log(`Found group ${group.publicKey.toBase58()}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
0,
'my_mango_account',
);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
// log users tokens
for (const token of mangoAccount.tokens) {
if (token.tokenIndex == TokenAccount.TokenIndexUnset) continue;
console.log(token.toString());
}
process.exit();
}
main();

View File

@ -39,7 +39,7 @@ async function main() {
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForAdmin(admin.publicKey);
const group = await client.getGroupForAdmin(admin.publicKey, 0);
console.log(`Found group ${group.publicKey.toBase58()}`);
// create + fetch account