Merge remote-tracking branch 'origin/dev' into ts/orca-margin-trade
This commit is contained in:
commit
4c5523c95b
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -1,3 +1,4 @@
|
|||
RPC_URL=
|
||||
PAYER_KEYPAIR=
|
||||
ADMIN_KEYPAIR=
|
||||
MANGO_ACCOUNT_NAME=
|
|
@ -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"] }
|
|
@ -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)
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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 &[_]))
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue