diff --git a/.github/workflows/ci-lint-test.yml b/.github/workflows/ci-lint-test.yml index e564abdab..fc8dcf218 100644 --- a/.github/workflows/ci-lint-test.yml +++ b/.github/workflows/ci-lint-test.yml @@ -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 - diff --git a/Cargo.lock b/Cargo.lock index 5af2d564a..a432fc6bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/keeper/Cargo.toml b/keeper/Cargo.toml index 6fbd03762..b413ab1aa 100644 --- a/keeper/Cargo.toml +++ b/keeper/Cargo.toml @@ -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"] } \ No newline at end of file diff --git a/keeper/src/consume_events.rs b/keeper/src/consume_events.rs deleted file mode 100644 index 5eef56ffd..000000000 --- a/keeper/src/consume_events.rs +++ /dev/null @@ -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, 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, - 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> { - let event_queue_opt: Option = { - 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) -} diff --git a/keeper/src/crank.rs b/keeper/src/crank.rs index a968cb690..79afff077 100644 --- a/keeper/src/crank.rs +++ b/keeper/src/crank.rs @@ -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, debugging_handle: impl Future, ) -> Result<(), anyhow::Error> { - // Collect all banks for a group belonging to an admin - let banks = mango_client - .program() - .accounts::(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::>(); - // 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::(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::>(); - 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::>(); futures::join!( @@ -87,3 +42,168 @@ pub async fn runner( Ok(()) } + +pub async fn loop_update_index(mango_client: Arc, 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, + 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, + 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(()) + }); + } +} diff --git a/keeper/src/main.rs b/keeper/src/main.rs index eda1aa0f4..cda1825cc 100644 --- a/keeper/src/main.rs +++ b/keeper/src/main.rs @@ -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, - #[clap(long, env = "PAYER_KEYPAIR")] + #[clap(short, long, env = "PAYER_KEYPAIR")] payer: Option, - #[clap(long, env = "PAYER_KEYPAIR_BASE58")] - payer_base58: Option, - - #[clap(long, env = "ADMIN_KEYPAIR")] + #[clap(short, long, env = "ADMIN_KEYPAIR")] admin: Option, - #[clap(long, env = "ADMIN_KEYPAIR_BASE58")] - admin_base58: Option, - #[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::strong_count() {}", + "Arc::strong_count() {}", Arc::::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(()) } diff --git a/keeper/src/mango_client.rs b/keeper/src/mango_client.rs new file mode 100644 index 000000000..5712150a7 --- /dev/null +++ b/keeper/src/mango_client.rs @@ -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, + pub banks_cache_by_token_index: HashMap, + pub mint_infos_cache: HashMap, + pub mint_infos_cache_by_token_index: HashMap, + pub serum3_markets_cache: HashMap, + pub serum3_external_markets_cache: HashMap)>, + pub perp_markets_cache: HashMap, +} + +// 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 { + 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::(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::(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::(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::(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::(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::(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, 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 { + 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 { + 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 { + 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 { + 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::()], + ); + 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 { + 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::()], + ); + 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, 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::()], + ); + + 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::()], + ); + 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 &[_])) +} diff --git a/keeper/src/taker.rs b/keeper/src/taker.rs new file mode 100644 index 000000000..45b6f0a5d --- /dev/null +++ b/keeper/src/taker.rs @@ -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, + _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::>() + .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::>(); + + 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::>(); + + futures::join!( + futures::future::join_all(handles1), + futures::future::join_all(handles2) + ); + + Ok(()) +} + +fn ensure_oo(mango_client: &Arc) -> 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) -> 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, + market_name: String, + price: Arc>, +) { + 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::>()[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, + market_name: String, + price: Arc>, +) { + let mut interval = time::interval(Duration::from_secs(5)); + + // Cancel existing orders + let orders: Vec = 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::(); + + 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(()) + }); + } +} diff --git a/keeper/src/update_funding.rs b/keeper/src/update_funding.rs deleted file mode 100644 index 3c2f83a3c..000000000 --- a/keeper/src/update_funding.rs +++ /dev/null @@ -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, 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, - 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(()) -} diff --git a/keeper/src/update_index.rs b/keeper/src/update_index.rs deleted file mode 100644 index 3fdca67c7..000000000 --- a/keeper/src/update_index.rs +++ /dev/null @@ -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, 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, - 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(()) -} diff --git a/keeper/src/util.rs b/keeper/src/util.rs new file mode 100644 index 000000000..ad7600d95 --- /dev/null +++ b/keeper/src/util.rs @@ -0,0 +1,27 @@ +use anyhow::anyhow; +use solana_sdk::signature::Keypair; + +#[allow(dead_code)] +pub fn retry(request: impl Fn() -> Result) -> anyhow::Result { + 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() + } +} diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 0cd9b4eff..077c5a0c5 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -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 } diff --git a/programs/mango-v4/src/state/serum3_market.rs b/programs/mango-v4/src/state/serum3_market.rs index 59f8edfa7..7204f25a7 100644 --- a/programs/mango-v4/src/state/serum3_market.rs +++ b/programs/mango-v4/src/state/serum3_market.rs @@ -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, diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index d201ec198..9d8b2efaf 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -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, diff --git a/ts/client/src/scripts/example1-ob.ts b/ts/client/src/scripts/example1-ob.ts new file mode 100644 index 000000000..ebfaa424a --- /dev/null +++ b/ts/client/src/scripts/example1-ob.ts @@ -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(); diff --git a/ts/client/src/scripts/example1-user-account.ts b/ts/client/src/scripts/example1-user-account.ts new file mode 100644 index 000000000..59c684890 --- /dev/null +++ b/ts/client/src/scripts/example1-user-account.ts @@ -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();