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:
microwavedcola1 2022-05-28 07:05:34 +02:00 committed by GitHub
parent cc2d46bf4a
commit 34a8f0919f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1306 additions and 424 deletions

View File

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

5
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -1,81 +1,36 @@
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
use crate::{consume_events, update_funding, update_index, MangoClient};
use anyhow::ensure;
use crate::MangoClient;
use anchor_lang::__private::bytemuck::cast_ref;
use futures::Future;
use mango_v4::state::{Bank, PerpMarket};
use solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType};
use solana_sdk::{pubkey::Pubkey, signer::Signer};
use mango_v4::state::{Bank, EventQueue, EventType, FillEvent, OutEvent, PerpMarket};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use tokio::time;
pub async fn runner(
mango_client: Arc<MangoClient>,
debugging_handle: impl Future,
) -> Result<(), anyhow::Error> {
// Collect all banks for a group belonging to an admin
let banks = mango_client
.program()
.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 24,
bytes: MemcmpEncodedBytes::Base58({
// find group belonging to admin
Pubkey::find_program_address(
&["Group".as_ref(), mango_client.admin.pubkey().as_ref()],
&mango_client.program().id(),
)
.0
.to_string()
}),
encoding: None,
})])?;
ensure!(!banks.is_empty());
let handles1 = banks
.iter()
.map(|(pk, bank)| update_index::loop_blocking(mango_client.clone(), *pk, *bank))
let handles1 = mango_client
.banks_cache
.values()
.map(|(pk, bank)| loop_update_index(mango_client.clone(), *pk, *bank))
.collect::<Vec<_>>();
// TODO: future, maybe we want to only consume events for specific markets,
// TODO: future, maybe we want to crank certain markets more often than others
// Collect all perp markets for a group belonging to an admin
let perp_markets =
mango_client
.program()
.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 24,
bytes: MemcmpEncodedBytes::Base58({
// find group belonging to admin
Pubkey::find_program_address(
&["Group".as_ref(), mango_client.admin.pubkey().as_ref()],
&mango_client.program().id(),
)
.0
.to_string()
}),
encoding: None,
})])?;
// TODO: enable
// ensure!(!perp_markets.is_empty());
// atm no perp code is deployed to devnet, and no perp markets have been init
let handles2 = perp_markets
.iter()
.map(|(pk, perp_market)| {
consume_events::loop_blocking(mango_client.clone(), *pk, *perp_market)
})
let handles2 = mango_client
.perp_markets_cache
.values()
.map(|(pk, perp_market)| loop_consume_events(mango_client.clone(), *pk, *perp_market))
.collect::<Vec<_>>();
let handles3 = perp_markets
.iter()
.map(|(pk, perp_market)| {
update_funding::loop_blocking(mango_client.clone(), *pk, *perp_market)
})
let handles3 = mango_client
.perp_markets_cache
.values()
.map(|(pk, perp_market)| loop_update_funding(mango_client.clone(), *pk, *perp_market))
.collect::<Vec<_>>();
futures::join!(
@ -87,3 +42,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(())
});
}
}

View File

@ -1,25 +1,20 @@
mod consume_events;
mod crank;
mod update_funding;
mod update_index;
mod mango_client;
mod taker;
mod util;
use std::env;
use std::sync::Arc;
use anchor_client::{Client, Cluster, Program};
use anchor_client::Cluster;
use clap::{Parser, Subcommand};
use solana_client::rpc_client::RpcClient;
use solana_sdk::signature::Keypair;
use solana_sdk::{
commitment_config::CommitmentConfig,
pubkey::Pubkey,
signer::{keypair, Signer},
};
use solana_sdk::{commitment_config::CommitmentConfig, signer::keypair};
use tokio::time;
use crate::mango_client::MangoClient;
// TODO
// - may be nice to have one-shot cranking as well as the interval cranking
// - doing a gPA for all banks call every 10millis may be too often,
@ -27,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(())
}

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

@ -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 &[_]))
}

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

@ -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(())
});
}
}

View File

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

View File

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

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

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

View File

@ -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
}

View File

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

View File

@ -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,

View File

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

View File

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