serum taker bot (#57)
* taker bot Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * inline code Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * cleanup Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * add mints Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * add todo Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * fix todos Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * remove stray log Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * cleanup Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * remove dead code Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * use same rust as what solana uses, use same solana version as cargo toml Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fix from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * fix from reviews Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * linter Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
cc2d46bf4a
commit
34a8f0919f
|
@ -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",
|
||||
|
|
|
@ -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,168 @@ 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();
|
||||
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(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,78 +22,18 @@ 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(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
@ -106,7 +41,7 @@ struct Cli {
|
|||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
Crank {},
|
||||
Liquidator {},
|
||||
Taker {},
|
||||
}
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
env_logger::init_from_env(
|
||||
|
@ -118,44 +53,20 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
let Cli {
|
||||
rpc_url,
|
||||
payer,
|
||||
payer_base58,
|
||||
admin,
|
||||
admin_base58,
|
||||
command,
|
||||
} = 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 +81,21 @@ 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));
|
||||
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());
|
||||
log::info!("Group {}", &mango_client.group());
|
||||
log::info!("User {}", &mango_client.payer());
|
||||
|
||||
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 +103,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 +113,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,704 @@
|
|||
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 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};
|
||||
|
||||
use crate::util::MyClone;
|
||||
|
||||
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,
|
||||
) -> 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()],
|
||||
&program.id(),
|
||||
)
|
||||
.0;
|
||||
|
||||
let mango_accounts = 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_cache = mango_accounts[0];
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
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,208 @@
|
|||
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 = mango_account.tokens.find(bank.token_index).unwrap();
|
||||
let native = token_account.native(bank);
|
||||
|
||||
let ui = token_account.ui(bank, mint);
|
||||
log::info!("Current balance {} {}", ui, bank.name());
|
||||
|
||||
let deposit_native = if native < I80F48::ZERO {
|
||||
desired_balance - native
|
||||
} else {
|
||||
desired_balance - native.min(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();
|
||||
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(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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(())
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -633,6 +633,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,
|
||||
|
|
|
@ -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();
|
Loading…
Reference in New Issue