diff --git a/client/src/client.rs b/client/src/client.rs index fb319cf4b..53d52db08 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -1,7 +1,9 @@ +use std::rc::Rc; use std::str::FromStr; use std::sync::Arc; +use std::time::Duration; -use anchor_client::{Client, ClientError, Cluster, Program}; +use anchor_client::{ClientError, Cluster, Program}; use anchor_lang::__private::bytemuck; use anchor_lang::prelude::System; @@ -28,17 +30,45 @@ use solana_sdk::signature::{Keypair, Signature}; use solana_sdk::sysvar; use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signer::Signer}; +// very close to anchor_client::Client, which unfortunately has no accessors or Clone +#[derive(Clone, Debug)] +pub struct Client { + pub cluster: Cluster, + pub fee_payer: Arc, + pub commitment: CommitmentConfig, +} + +impl Client { + pub fn new(cluster: Cluster, commitment: CommitmentConfig, fee_payer: &Keypair) -> Self { + Self { + cluster, + fee_payer: Arc::new(fee_payer.clone()), + commitment, + } + } + + pub fn anchor_client(&self) -> anchor_client::Client { + anchor_client::Client::new_with_options( + self.cluster.clone(), + Rc::new((*self.fee_payer).clone()), + self.commitment, + ) + } + + pub fn rpc_with_timeout(&self, timeout: Duration) -> RpcClient { + RpcClient::new_with_timeout_and_commitment(self.cluster.clone(), timeout, self.commitment) + } +} + // todo: might want to integrate geyser, websockets, or simple http polling for keeping data fresh pub struct MangoClient { - pub rpc: RpcClient, - pub cluster: Cluster, - pub commitment: CommitmentConfig, + pub client: Client, // todo: possibly this object should have cache-functions, so there can be one getMultipleAccounts // call to refresh banks etc -- if it's backed by websockets, these could just do nothing pub account_fetcher: Arc, - pub payer: Keypair, + pub owner: Keypair, pub mango_account_address: Pubkey, pub context: MangoGroupContext, @@ -58,48 +88,17 @@ impl MangoClient { .0 } - /// Conveniently creates a RPC based client - pub fn new( - cluster: Cluster, - commitment: CommitmentConfig, + pub fn find_or_create_account( + client: &Client, group: Pubkey, - payer: Keypair, + owner: Keypair, + payer: Keypair, // pays the SOL for the new account mango_account_name: &str, - ) -> anyhow::Result { - let group_context = MangoGroupContext::new_from_rpc(group, cluster.clone(), commitment)?; - - let rpc = RpcClient::new_with_commitment(cluster.url().to_string(), commitment); - let account_fetcher = Arc::new(CachedAccountFetcher::new(RpcAccountFetcher { rpc })); - - Self::new_detail( - cluster, - commitment, - payer, - mango_account_name, - group_context, - account_fetcher, - ) - } - - /// Allows control of AccountFetcher and externally created MangoGroupContext - pub fn new_detail( - cluster: Cluster, - commitment: CommitmentConfig, - payer: Keypair, - mango_account_name: &str, - // future: maybe pass Arc, so it can be extenally updated? - group_context: MangoGroupContext, - account_fetcher: Arc, - ) -> 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 = group_context.group; + ) -> anyhow::Result { + let program = client.anchor_client().program(mango_v4::ID); // Mango Account - let mut mango_account_tuples = fetch_mango_accounts(&program, group, payer.pubkey())?; + let mut mango_account_tuples = fetch_mango_accounts(&program, group, owner.pubkey())?; let mango_account_opt = mango_account_tuples .iter() .find(|(_, account)| account.fixed.name() == mango_account_name); @@ -121,13 +120,13 @@ impl MangoClient { accounts: anchor_lang::ToAccountMetas::to_account_metas( &mango_v4::accounts::AccountCreate { group, - owner: payer.pubkey(), + owner: owner.pubkey(), account: { Pubkey::find_program_address( &[ group.as_ref(), b"MangoAccount".as_ref(), - payer.pubkey().as_ref(), + owner.pubkey().as_ref(), &account_num.to_le_bytes(), ], &mango_v4::id(), @@ -147,42 +146,72 @@ impl MangoClient { }, ), }) + .signer(&owner) + .signer(&payer) .send() .map_err(prettify_client_error) .context("Failed to create account...")?; } - let mango_account_tuples = fetch_mango_accounts(&program, group, payer.pubkey())?; + let mango_account_tuples = fetch_mango_accounts(&program, group, owner.pubkey())?; let index = mango_account_tuples .iter() .position(|tuple| tuple.1.fixed.name() == mango_account_name) .unwrap(); - let mango_account_cache = &mango_account_tuples[index]; + Ok(mango_account_tuples[index].0) + } + /// Conveniently creates a RPC based client + pub fn new_for_existing_account( + client: Client, + account: Pubkey, + owner: Keypair, + ) -> anyhow::Result { + let rpc = client.rpc_with_timeout(Duration::from_secs(60)); + let account_fetcher = Arc::new(CachedAccountFetcher::new(RpcAccountFetcher { rpc })); + let mango_account = account_fetcher_fetch_mango_account(&*account_fetcher, account)?; + let group = mango_account.fixed.group; + if mango_account.fixed.owner != owner.pubkey() { + anyhow::bail!( + "bad owner for account: expected {} got {}", + mango_account.fixed.owner, + owner.pubkey() + ); + } + + let group_context = + MangoGroupContext::new_from_rpc(group, client.cluster.clone(), client.commitment)?; + + Self::new_detail(client, account, owner, group_context, account_fetcher) + } + + /// Allows control of AccountFetcher and externally created MangoGroupContext + pub fn new_detail( + client: Client, + account: Pubkey, + owner: Keypair, + // future: maybe pass Arc, so it can be extenally updated? + group_context: MangoGroupContext, + account_fetcher: Arc, + ) -> anyhow::Result { Ok(Self { - rpc, - cluster, - commitment, + client, account_fetcher, - payer, - mango_account_address: mango_account_cache.0, + owner, + mango_account_address: account, context: group_context, }) } - pub fn client(&self) -> Client { - Client::new_with_options( - self.cluster.clone(), - std::rc::Rc::new(self.payer.clone()), - self.commitment, - ) + pub fn anchor_client(&self) -> anchor_client::Client { + self.client.anchor_client() } pub fn program(&self) -> Program { - self.client().program(mango_v4::ID) + self.anchor_client().program(mango_v4::ID) } - pub fn payer(&self) -> Pubkey { - self.payer.pubkey() + pub fn owner(&self) -> Pubkey { + self.owner.pubkey() } pub fn group(&self) -> Pubkey { @@ -282,10 +311,10 @@ impl MangoClient { bank: mint_info.first_bank(), vault: mint_info.first_vault(), token_account: get_associated_token_address( - &self.payer(), + &self.owner(), &mint_info.mint, ), - token_authority: self.payer(), + token_authority: self.owner(), token_program: Token::id(), }, None, @@ -297,6 +326,7 @@ impl MangoClient { amount, }), }) + .signer(&self.owner) .send() .map_err(prettify_client_error) } @@ -348,8 +378,8 @@ impl MangoClient { serum_program: serum3_info.market.serum_program, serum_market_external: serum3_info.market.serum_market_external, open_orders, - owner: self.payer(), - payer: self.payer(), + owner: self.owner(), + payer: self.owner(), system_program: System::id(), rent: sysvar::rent::id(), }, @@ -359,6 +389,7 @@ impl MangoClient { &mango_v4::instruction::Serum3CreateOpenOrders {}, ), }) + .signer(&self.owner) .send() .map_err(prettify_client_error) } @@ -482,7 +513,7 @@ impl MangoClient { market_base_vault: s3.market.coin_vault, market_quote_vault: s3.market.pc_vault, market_vault_signer: s3.market.vault_signer, - owner: self.payer(), + owner: self.owner(), token_program: Token::id(), }, None, @@ -503,6 +534,7 @@ impl MangoClient { }, ), }) + .signer(&self.owner) .send() .map_err(prettify_client_error) } @@ -532,7 +564,7 @@ impl MangoClient { market_base_vault: s3.market.coin_vault, market_quote_vault: s3.market.pc_vault, market_vault_signer: s3.market.vault_signer, - owner: self.payer(), + owner: self.owner(), token_program: Token::id(), }, None, @@ -541,6 +573,7 @@ impl MangoClient { &mango_v4::instruction::Serum3SettleFunds {}, ), }) + .signer(&self.owner) .send() .map_err(prettify_client_error) } @@ -602,7 +635,7 @@ impl MangoClient { market_bids: s3.market.bids, market_asks: s3.market.asks, market_event_queue: s3.market.event_q, - owner: self.payer(), + owner: self.owner(), }, None, ) @@ -611,6 +644,7 @@ impl MangoClient { &mango_v4::instruction::Serum3CancelOrder { side, order_id }, ), }) + .signer(&self.owner) .send() .map_err(prettify_client_error)?; @@ -650,7 +684,7 @@ impl MangoClient { group: self.group(), liqee: *liqee.0, liqor: self.mango_account_address, - liqor_owner: self.payer.pubkey(), + liqor_owner: self.owner(), }, None, ); @@ -665,6 +699,7 @@ impl MangoClient { }, ), }) + .signer(&self.owner) .send() .map_err(prettify_client_error) } @@ -714,7 +749,7 @@ impl MangoClient { group: self.group(), liqee: *liqee.0, liqor: self.mango_account_address, - liqor_owner: self.payer.pubkey(), + liqor_owner: self.owner(), liab_mint_info: liab_info.mint_info_address, quote_vault: quote_info.mint_info.first_vault(), insurance_vault: group.insurance_vault, @@ -733,6 +768,7 @@ impl MangoClient { }, ), }) + .signer(&self.owner) .send() .map_err(prettify_client_error) } diff --git a/keeper/.env.example b/keeper/.env.example index ec532c91e..efceeca8c 100644 --- a/keeper/.env.example +++ b/keeper/.env.example @@ -1,4 +1,3 @@ RPC_URL= -PAYER_KEYPAIR= -GROUP= -MANGO_ACCOUNT_NAME= \ No newline at end of file +MANGO_ACCOUNT= +OWNER= diff --git a/keeper/src/main.rs b/keeper/src/main.rs index babd896d3..88033de62 100644 --- a/keeper/src/main.rs +++ b/keeper/src/main.rs @@ -6,9 +6,9 @@ use std::sync::Arc; use anchor_client::Cluster; use clap::{Parser, Subcommand}; -use client::{keypair_from_cli, MangoClient}; +use client::{keypair_from_cli, Client, MangoClient}; +use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::pubkey::Pubkey; -use solana_sdk::{commitment_config::CommitmentConfig, signature::Signer}; use tokio::time; // TODO @@ -34,21 +34,11 @@ struct Cli { #[clap(short, long, env)] rpc_url: String, - #[clap(short, long, env = "PAYER_KEYPAIR")] - payer: String, + #[clap(short, long, env)] + mango_account: Pubkey, #[clap(short, long, env)] - group: Option, - - // These exist only as a shorthand to make testing easier. Normal users would provide the group. - #[clap(long, env)] - group_from_admin_keypair: Option, - - #[clap(long, env, default_value = "0")] - group_from_admin_num: u32, - - #[clap(short, long, env)] - mango_account_name: String, + owner: String, #[clap(subcommand)] command: Command, @@ -73,7 +63,7 @@ fn main() -> Result<(), anyhow::Error> { }; let cli = Cli::parse_from(args); - let payer = keypair_from_cli(&cli.payer); + let owner = keypair_from_cli(&cli.owner); let rpc_url = cli.rpc_url; let ws_url = rpc_url.replace("https", "wss"); @@ -84,21 +74,10 @@ fn main() -> Result<(), anyhow::Error> { Command::Taker { .. } => CommitmentConfig::confirmed(), }; - let group = if let Some(group) = cli.group { - group - } else if let Some(p) = cli.group_from_admin_keypair { - let admin = keypair_from_cli(&p); - MangoClient::group_for_admin(admin.pubkey(), cli.group_from_admin_num) - } else { - panic!("Must provide either group or group_from_admin_keypair"); - }; - - let mango_client = Arc::new(MangoClient::new( - cluster, - commitment, - group, - payer, - &cli.mango_account_name, + let mango_client = Arc::new(MangoClient::new_for_existing_account( + Client::new(cluster, commitment, &owner), + cli.mango_account, + owner, )?); let rt = tokio::runtime::Builder::new_multi_thread() diff --git a/liquidator/.env.example b/liquidator/.env.example index 40c51844b..c9ff08c3c 100644 --- a/liquidator/.env.example +++ b/liquidator/.env.example @@ -1,6 +1,4 @@ RPC_URL=https://mango.devnet.rpcpool.com -GROUP=9njq91AuvDD6MXxr1uPiHY1B5ACHv6r6z5ZrCpz7sqtQ -PYTH_PROGRAM=gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s SERUM_PROGRAM=DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY +LIQOR_MANGO_ACCOUNT= LIQOR_OWNER=~/.config/solana/user.json -LIQOR_MANGO_ACCOUNT_NAME=my_mango_account diff --git a/liquidator/src/main.rs b/liquidator/src/main.rs index 245260e2c..e45e52a3e 100644 --- a/liquidator/src/main.rs +++ b/liquidator/src/main.rs @@ -4,14 +4,12 @@ use std::time::Duration; use anchor_client::Cluster; use clap::Parser; -use client::{chain_data, keypair_from_cli, MangoClient, MangoGroupContext}; +use client::{chain_data, keypair_from_cli, Client, MangoClient, MangoGroupContext}; use log::*; use mango_v4::state::{PerpMarketIndex, TokenIndex}; -use solana_client::rpc_client::RpcClient; use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::pubkey::Pubkey; -use solana_sdk::signer::Signer; use std::collections::HashSet; pub mod account_shared_data; @@ -56,29 +54,15 @@ struct Cli { #[clap(short, long, env)] rpc_url: String, - #[clap(long, env)] - group: Option, - - // These exist only as a shorthand to make testing easier. Normal users would provide the group. - #[clap(long, env)] - group_from_admin_keypair: Option, - - #[clap(long, env, default_value = "0")] - group_from_admin_num: u32, - - // TODO: maybe store this in the group, so it's easy to start without providing it? - #[clap(long, env)] - pyth_program: Pubkey, - // TODO: different serum markets could use different serum programs, should come from registered markets #[clap(long, env)] serum_program: Pubkey, #[clap(long, env)] - liqor_owner: String, + liqor_mango_account: Pubkey, #[clap(long, env)] - liqor_mango_account_name: String, + liqor_owner: String, #[clap(long, env, default_value = "300")] snapshot_interval_secs: u64, @@ -109,21 +93,25 @@ async fn main() -> anyhow::Result<()> { let liqor_owner = keypair_from_cli(&cli.liqor_owner); - let mango_group = if let Some(group) = cli.group { - group - } else if let Some(p) = cli.group_from_admin_keypair { - let admin = keypair_from_cli(&p); - MangoClient::group_for_admin(admin.pubkey(), cli.group_from_admin_num) - } else { - panic!("Must provide either group or group_from_admin_keypair"); - }; - let rpc_url = cli.rpc_url; let ws_url = rpc_url.replace("https", "wss"); let rpc_timeout = Duration::from_secs(1); let cluster = Cluster::Custom(rpc_url.clone(), ws_url.clone()); let commitment = CommitmentConfig::processed(); + let client = Client::new(cluster.clone(), commitment, &liqor_owner); + + // The representation of current on-chain account data + let chain_data = Arc::new(RwLock::new(chain_data::ChainData::new())); + // Reading accounts from chain_data + let account_fetcher = Arc::new(chain_data::AccountFetcher { + chain_data: chain_data.clone(), + rpc: client.rpc_with_timeout(rpc_timeout), + }); + + let mango_account = account_fetcher.fetch_fresh_mango_account(&cli.liqor_mango_account)?; + let mango_group = mango_account.fixed.group; + let group_context = MangoGroupContext::new_from_rpc(mango_group, cluster.clone(), commitment)?; // TODO: this is all oracles, not just pyth! @@ -185,18 +173,6 @@ async fn main() -> anyhow::Result<()> { snapshot_sender, ); - // The representation of current on-chain account data - let chain_data = Arc::new(RwLock::new(chain_data::ChainData::new())); - // Reading accounts from chain_data - let account_fetcher = Arc::new(chain_data::AccountFetcher { - chain_data: chain_data.clone(), - rpc: RpcClient::new_with_timeout_and_commitment( - cluster.url().to_string(), - rpc_timeout, - commitment, - ), - }); - start_chain_data_metrics(chain_data.clone(), &metrics); // Addresses of the MangoAccounts belonging to the mango program. @@ -227,10 +203,9 @@ async fn main() -> anyhow::Result<()> { // let mango_client = { Arc::new(MangoClient::new_detail( - cluster, - commitment, + client, + cli.liqor_mango_account, liqor_owner, - &cli.liqor_mango_account_name, group_context, account_fetcher.clone(), )?)