diff --git a/Cargo.lock b/Cargo.lock index b45c8530..e693e3aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2383,6 +2383,8 @@ dependencies = [ "anchor-client", "anchor-spl", "anyhow", + "bincode", + "bs58 0.4.0", "bytemuck", "chrono", "clap 3.2.25", diff --git a/clients/rust/marginfi-cli/Cargo.toml b/clients/rust/marginfi-cli/Cargo.toml index 9047f941..9d546897 100644 --- a/clients/rust/marginfi-cli/Cargo.toml +++ b/clients/rust/marginfi-cli/Cargo.toml @@ -42,3 +42,5 @@ spl-token = "3.5.0" spl-associated-token-account = "1.1.2" chrono = "0.4.23" switchboard-v2 = "0.1.22" +bincode = "1.3.1" +bs58 = "0.4.0" diff --git a/clients/rust/marginfi-cli/src/config.rs b/clients/rust/marginfi-cli/src/config.rs index 5842bfe6..1175e07f 100644 --- a/clients/rust/marginfi-cli/src/config.rs +++ b/clients/rust/marginfi-cli/src/config.rs @@ -1,8 +1,14 @@ -use anchor_client::{Client, Cluster, Program}; -use clap::Parser; -use serde::{Deserialize, Serialize}; -use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair}; -use std::str::FromStr; +use { + anchor_client::{Client, Cluster, Program}, + clap::Parser, + serde::{Deserialize, Serialize}, + solana_sdk::{ + commitment_config::CommitmentConfig, + pubkey::Pubkey, + signature::{Keypair, Signer}, + }, + std::str::FromStr, +}; #[derive(Default, Debug, Parser)] pub struct GlobalOptions { @@ -32,9 +38,23 @@ pub struct GlobalOptions { pub skip_confirmation: bool, } +pub enum CliSigner { + Keypair(Keypair), + Multisig(Pubkey), +} + +impl CliSigner { + pub fn pubkey(&self) -> Pubkey { + match self { + CliSigner::Keypair(keypair) => keypair.pubkey(), + CliSigner::Multisig(pubkey) => *pubkey, + } + } +} + pub struct Config { pub cluster: Cluster, - pub payer: Keypair, + pub signer: CliSigner, pub program_id: Pubkey, pub commitment: CommitmentConfig, pub dry_run: bool, diff --git a/clients/rust/marginfi-cli/src/entrypoint.rs b/clients/rust/marginfi-cli/src/entrypoint.rs index 51f2f1b3..5b161690 100644 --- a/clients/rust/marginfi-cli/src/entrypoint.rs +++ b/clients/rust/marginfi-cli/src/entrypoint.rs @@ -277,7 +277,9 @@ pub enum ProfileCommand { #[clap(long)] cluster: Cluster, #[clap(long)] - keypair_path: String, + keypair_path: Option, + #[clap(long)] + multisig: Option, #[clap(long)] rpc_url: String, #[clap(long)] @@ -301,6 +303,8 @@ pub enum ProfileCommand { #[clap(long)] keypair_path: Option, #[clap(long)] + multisig: Option, + #[clap(long)] rpc_url: Option, #[clap(long)] program_id: Option, @@ -388,6 +392,7 @@ fn profile(subcmd: ProfileCommand) -> Result<()> { name, cluster, keypair_path, + multisig, rpc_url, program_id, commitment, @@ -397,6 +402,7 @@ fn profile(subcmd: ProfileCommand) -> Result<()> { name, cluster, keypair_path, + multisig, rpc_url, program_id, commitment, @@ -409,6 +415,7 @@ fn profile(subcmd: ProfileCommand) -> Result<()> { ProfileCommand::Update { cluster, keypair_path, + multisig, rpc_url, program_id, commitment, @@ -419,6 +426,7 @@ fn profile(subcmd: ProfileCommand) -> Result<()> { name, cluster, keypair_path, + multisig, rpc_url, program_id, commitment, @@ -510,6 +518,7 @@ fn bank(subcmd: BankCommand, global_options: &GlobalOptions) -> Result<()> { BankCommand::Get { .. } | BankCommand::GetAll { .. } => (), #[cfg(feature = "dev")] BankCommand::InspectPriceOracle { .. } => (), + #[allow(unreachable_patterns)] _ => get_consent(&subcmd, &profile)?, } } diff --git a/clients/rust/marginfi-cli/src/processor/emissions.rs b/clients/rust/marginfi-cli/src/processor/emissions.rs index bb2a6ed9..02d2830f 100644 --- a/clients/rust/marginfi-cli/src/processor/emissions.rs +++ b/clients/rust/marginfi-cli/src/processor/emissions.rs @@ -1,23 +1,34 @@ -use anchor_client::anchor_lang::{AnchorSerialize, InstructionData, ToAccountMetas}; -use anyhow::Result; -use marginfi::state::marginfi_account::MarginfiAccount; -use solana_client::rpc_filter::{Memcmp, RpcFilterType}; -use solana_sdk::{ - instruction::Instruction, pubkey::Pubkey, signer::Signer, transaction::Transaction, +use { + crate::{ + config::{CliSigner, Config}, + profile::Profile, + }, + anchor_client::anchor_lang::{AnchorSerialize, InstructionData, ToAccountMetas}, + anyhow::Result, + marginfi::state::marginfi_account::MarginfiAccount, + solana_client::rpc_filter::{Memcmp, RpcFilterType}, + solana_sdk::{ + instruction::Instruction, message::Message, pubkey::Pubkey, transaction::Transaction, + }, }; -use crate::{config::Config, profile::Profile}; - -#[cfg(feature = "admin")] const CHUNK_SIZE: usize = 22; -#[cfg(feature = "admin")] + pub fn claim_all_emissions_for_bank( config: &Config, profile: &Profile, bank_pk: Pubkey, ) -> Result<()> { + let rpc_client = config.mfi_program.rpc(); + let group = profile.marginfi_group.expect("group not set"); + let signing_keypairs = if let CliSigner::Keypair(keypair) = &config.signer { + vec![keypair] + } else { + vec![] + }; + let marginfi_accounts = config .mfi_program @@ -60,19 +71,13 @@ pub fn claim_all_emissions_for_bank( println!("Sending {} txs", ixs_batches_count); for (i, ixs) in ixs_batches.enumerate() { - let blockhash = config.mfi_program.rpc().get_latest_blockhash()?; + let blockhash = rpc_client.get_latest_blockhash()?; - let tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.payer.pubkey()), - &[&config.payer], - blockhash, - ); + let message = Message::new(ixs, Some(&config.signer.pubkey())); + let mut transaction = Transaction::new_unsigned(message); + transaction.partial_sign(&signing_keypairs, blockhash); - let sig = config - .mfi_program - .rpc() - .send_and_confirm_transaction_with_spinner(&tx)?; + let sig = rpc_client.send_and_confirm_transaction_with_spinner(&transaction)?; println!("Sent [{}/{}] {}", i + 1, ixs_batches_count, sig); } diff --git a/clients/rust/marginfi-cli/src/processor/mod.rs b/clients/rust/marginfi-cli/src/processor/mod.rs index 7dd004d9..c3cfa547 100644 --- a/clients/rust/marginfi-cli/src/processor/mod.rs +++ b/clients/rust/marginfi-cli/src/processor/mod.rs @@ -1,69 +1,80 @@ #[cfg(feature = "admin")] -use crate::utils::{create_oracle_key_array, find_bank_vault_pda}; -use crate::{ - config::Config, - profile::{self, get_cli_config_dir, load_profile, CliConfig, Profile}, - utils::{ - find_bank_emssions_auth_pda, find_bank_emssions_token_account_pda, - find_bank_vault_authority_pda, load_observation_account_metas, process_transaction, - EXP_10_I80F48, +pub mod emissions; + +use { + crate::{ + config::{CliSigner, Config}, + profile::{self, get_cli_config_dir, load_profile, CliConfig, Profile}, + utils::{ + find_bank_vault_authority_pda, load_observation_account_metas, process_transaction, + EXP_10_I80F48, + }, }, -}; -use anchor_client::{ - anchor_lang::{InstructionData, ToAccountMetas}, - Cluster, -}; -use anchor_spl::token::{self, spl_token}; -use anyhow::{anyhow, bail, Result}; -#[cfg(feature = "lip")] -use chrono::{DateTime, NaiveDateTime, Utc}; -use fixed::types::I80F48; -#[cfg(feature = "lip")] -use liquidity_incentive_program::state::{Campaign, Deposit}; -use log::info; -use marginfi::{ - constants::{EMISSIONS_FLAG_BORROW_ACTIVE, EMISSIONS_FLAG_LENDING_ACTIVE}, - prelude::MarginfiGroup, - state::{ - marginfi_account::{BankAccountWrapper, MarginfiAccount}, - marginfi_group::{Bank, BankVaultType}, - price::{OraclePriceFeedAdapter, PriceAdapter}, + anchor_client::{ + anchor_lang::{InstructionData, ToAccountMetas}, + Cluster, + }, + anchor_spl::token::{self, spl_token}, + anyhow::{anyhow, bail, Result}, + fixed::types::I80F48, + log::info, + marginfi::{ + prelude::MarginfiGroup, + state::{ + marginfi_account::{BankAccountWrapper, MarginfiAccount}, + marginfi_group::{Bank, BankVaultType}, + }, + }, + solana_client::rpc_filter::{Memcmp, RpcFilterType}, + solana_sdk::{ + account_info::IntoAccountInfo, + clock::Clock, + commitment_config::CommitmentLevel, + compute_budget::ComputeBudgetInstruction, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + system_program, + sysvar::{self, Sysvar}, + transaction::Transaction, + }, + spl_associated_token_account::instruction::create_associated_token_account_idempotent, + std::{ + collections::HashMap, + fs, + mem::size_of, + ops::{Neg, Not}, + time::{Duration, SystemTime, UNIX_EPOCH}, }, }; + +#[cfg(feature = "dev")] +use marginfi::state::price::{OraclePriceFeedAdapter, PriceAdapter}; + #[cfg(feature = "admin")] -use marginfi::{ - prelude::GroupConfig, - state::marginfi_group::{ - BankConfig, BankConfigOpt, BankOperationalState, InterestRateConfig, WrappedI80F48, +use { + crate::utils::{ + calc_emissions_rate, create_oracle_key_array, find_bank_emssions_auth_pda, + find_bank_emssions_token_account_pda, find_bank_vault_pda, }, -}; -use solana_client::rpc_filter::{Memcmp, RpcFilterType}; -use solana_sdk::instruction::AccountMeta; -use solana_sdk::{ - account_info::IntoAccountInfo, - clock::Clock, - commitment_config::CommitmentLevel, - instruction::Instruction, - pubkey::Pubkey, - signature::Keypair, - signer::Signer, - system_program, - sysvar::{self, Sysvar}, - transaction::Transaction, -}; -use solana_sdk::{compute_budget::ComputeBudgetInstruction, program_pack::Pack}; -use spl_associated_token_account::{ - get_associated_token_address, instruction::create_associated_token_account_idempotent, -}; -use std::{ - collections::HashMap, - fs, io, - mem::size_of, - ops::{Neg, Not}, - time::{Duration, SystemTime, UNIX_EPOCH}, + marginfi::{ + constants::{EMISSIONS_FLAG_BORROW_ACTIVE, EMISSIONS_FLAG_LENDING_ACTIVE}, + prelude::GroupConfig, + state::marginfi_group::{ + BankConfig, BankConfigOpt, BankOperationalState, InterestRateConfig, WrappedI80F48, + }, + }, + solana_sdk::{message::Message, program_pack::Pack}, + spl_associated_token_account::get_associated_token_address, + std::io, }; -pub mod emissions; +#[cfg(feature = "lip")] +use { + chrono::{DateTime, NaiveDateTime, Utc}, + liquidity_incentive_program::state::{Campaign, Deposit}, +}; // -------------------------------------------------------------------------------------------------------------------- // marginfi group @@ -196,7 +207,7 @@ pub fn group_create( override_existing_profile_group: bool, ) -> Result<()> { let rpc_client = config.mfi_program.rpc(); - let admin = admin.unwrap_or_else(|| config.payer.pubkey()); + let admin = admin.unwrap_or_else(|| config.signer.pubkey()); if profile.marginfi_group.is_some() && !override_existing_profile_group { bail!( @@ -207,10 +218,16 @@ pub fn group_create( let marginfi_group_keypair = Keypair::new(); - let init_marginfi_group_ix = config - .mfi_program - .request() - .signer(&config.payer) + let mut init_marginfi_group_ixs_builder = config.mfi_program.request(); + + let signing_keypairs = if let CliSigner::Keypair(keypair) = &config.signer { + init_marginfi_group_ixs_builder = init_marginfi_group_ixs_builder.signer(keypair); + vec![keypair, &marginfi_group_keypair] + } else { + vec![&marginfi_group_keypair] + }; + + let init_marginfi_group_ixs = init_marginfi_group_ixs_builder .accounts(marginfi::accounts::MarginfiGroupInitialize { marginfi_group: marginfi_group_keypair.pubkey(), admin, @@ -220,16 +237,11 @@ pub fn group_create( .instructions()?; let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); + let message = Message::new(&init_marginfi_group_ixs, Some(&config.signer.pubkey())); + let mut transaction = Transaction::new_unsigned(message); + transaction.partial_sign(&signing_keypairs, recent_blockhash); - let signers = vec![&config.payer, &marginfi_group_keypair]; - let tx = Transaction::new_signed_with_payer( - &init_marginfi_group_ix, - Some(&config.payer.pubkey()), - &signers, - recent_blockhash, - ); - - match process_transaction(&tx, &rpc_client, config.dry_run) { + match process_transaction(&transaction, &rpc_client, config.dry_run, &config.signer) { Ok(sig) => println!("marginfi group created (sig: {})", sig), Err(err) => { println!("Error during marginfi group creation:\n{:#?}", err); @@ -251,13 +263,19 @@ pub fn group_configure(config: Config, profile: Profile, admin: Option) bail!("Marginfi group not specified in profile [{}]", profile.name); } - let configure_marginfi_group_ix = config - .mfi_program - .request() - .signer(&config.payer) + let mut configure_marginfi_group_ixs_builder = config.mfi_program.request(); + + let signing_keypairs = if let CliSigner::Keypair(keypair) = &config.signer { + configure_marginfi_group_ixs_builder = configure_marginfi_group_ixs_builder.signer(keypair); + vec![keypair] + } else { + vec![] + }; + + let configure_marginfi_group_ixs = configure_marginfi_group_ixs_builder .accounts(marginfi::accounts::MarginfiGroupConfigure { marginfi_group: profile.marginfi_group.unwrap(), - admin: config.payer.pubkey(), + admin: config.signer.pubkey(), }) .args(marginfi::instruction::MarginfiGroupConfigure { config: GroupConfig { admin }, @@ -265,16 +283,11 @@ pub fn group_configure(config: Config, profile: Profile, admin: Option) .instructions()?; let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); + let message = Message::new(&configure_marginfi_group_ixs, Some(&config.signer.pubkey())); + let mut transaction = Transaction::new_unsigned(message); + transaction.partial_sign(&signing_keypairs, recent_blockhash); - let signers = vec![&config.payer]; - let tx = Transaction::new_signed_with_payer( - &configure_marginfi_group_ix, - Some(&config.payer.pubkey()), - &signers, - recent_blockhash, - ); - - match process_transaction(&tx, &rpc_client, config.dry_run) { + match process_transaction(&transaction, &rpc_client, config.dry_run, &config.signer) { Ok(sig) => println!("marginfi group created (sig: {})", sig), Err(err) => println!("Error during marginfi group creation:\n{:#?}", err), }; @@ -337,13 +350,19 @@ pub fn group_add_bank( let bank_keypair = Keypair::new(); - let add_bank_ix = config - .mfi_program - .request() - .signer(&config.payer) + let mut add_bank_ixs_builder = config.mfi_program.request(); + + let signing_keypairs = if let CliSigner::Keypair(keypair) = &config.signer { + add_bank_ixs_builder = add_bank_ixs_builder.signer(keypair); + vec![keypair, &bank_keypair] + } else { + vec![&bank_keypair] + }; + + let add_bank_ixs = add_bank_ixs_builder .accounts(marginfi::accounts::LendingPoolAddBank { marginfi_group: profile.marginfi_group.unwrap(), - admin: config.payer.pubkey(), + admin: config.signer.pubkey(), bank: bank_keypair.pubkey(), bank_mint, fee_vault: find_bank_vault_pda( @@ -406,16 +425,11 @@ pub fn group_add_bank( .instructions()?; let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); + let message = Message::new(&add_bank_ixs, Some(&config.signer.pubkey())); + let mut transaction = Transaction::new_unsigned(message); + transaction.partial_sign(&signing_keypairs, recent_blockhash); - let signers = vec![&config.payer, &bank_keypair]; - let tx = Transaction::new_signed_with_payer( - &add_bank_ix, - Some(&config.payer.pubkey()), - &signers, - recent_blockhash, - ); - - match process_transaction(&tx, &rpc_client, config.dry_run) { + match process_transaction(&transaction, &rpc_client, config.dry_run, &config.signer) { Ok(sig) => println!("bank created (sig: {})", sig), Err(err) => println!("Error during bank creation:\n{:#?}", err), }; @@ -451,7 +465,7 @@ pub fn group_handle_bankruptcy( program_id: config.program_id, accounts: marginfi::accounts::LendingPoolHandleBankruptcy { marginfi_group: profile.marginfi_group.unwrap(), - admin: config.payer.pubkey(), + admin: config.signer.pubkey(), bank: bank_pk, marginfi_account: marginfi_account_pk, liquidity_vault: find_bank_vault_pda( @@ -488,16 +502,16 @@ pub fn group_handle_bankruptcy( )); let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); + let signing_keypairs = if let CliSigner::Keypair(keypair) = &config.signer { + vec![keypair] + } else { + vec![] + }; + let message = Message::new(&[handle_bankruptcy_ix], Some(&config.signer.pubkey())); + let mut transaction = Transaction::new_unsigned(message); + transaction.partial_sign(&signing_keypairs, recent_blockhash); - let signers = vec![&config.payer]; - let tx = Transaction::new_signed_with_payer( - &[handle_bankruptcy_ix], - Some(&config.payer.pubkey()), - &signers, - recent_blockhash, - ); - - match process_transaction(&tx, &rpc_client, config.dry_run) { + match process_transaction(&transaction, &rpc_client, config.dry_run, &config.signer) { Ok(sig) => println!("Bankruptcy handled (sig: {})", sig), Err(err) => println!("Error during bankruptcy handling:\n{:#?}", err), }; @@ -575,6 +589,7 @@ pub fn bank_get_all(config: Config, marginfi_group: Option) -> Result<() Ok(()) } +#[cfg(feature = "dev")] pub fn bank_inspect_price_oracle(config: Config, bank_pk: Pubkey) -> Result<()> { let bank: Bank = config.mfi_program.account(bank_pk)?; let mut price_oracle_account = config @@ -624,7 +639,9 @@ pub fn bank_setup_emissions( rate: f64, total: f64, ) -> Result<()> { - let funding_account_ata = get_associated_token_address(&config.payer.pubkey(), &mint); + let rpc_client = config.mfi_program.rpc(); + + let funding_account_ata = get_associated_token_address(&config.signer.pubkey(), &mint); let mut flags = 0; if deposits { @@ -667,7 +684,7 @@ pub fn bank_setup_emissions( program_id: marginfi::id(), accounts: marginfi::accounts::LendingPoolSetupEmissions { marginfi_group: profile.marginfi_group.expect("marginfi group not set"), - admin: config.payer.pubkey(), + admin: config.signer.pubkey(), bank, emissions_mint: mint, emissions_auth: find_bank_emssions_auth_pda(bank, mint, marginfi::id()).0, @@ -690,16 +707,17 @@ pub fn bank_setup_emissions( .data(), }; - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&config.payer.pubkey()), - &[&config.payer], - config.mfi_program.rpc().get_latest_blockhash().unwrap(), - ); - - let rpc_program = config.mfi_program.rpc(); + let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); + let signing_keypairs = if let CliSigner::Keypair(keypair) = &config.signer { + vec![keypair] + } else { + vec![] + }; + let message = Message::new(&[ix], Some(&config.signer.pubkey())); + let mut transaction = Transaction::new_unsigned(message); + transaction.partial_sign(&signing_keypairs, recent_blockhash); - match process_transaction(&tx, &rpc_program, config.dry_run) { + match process_transaction(&transaction, &rpc_client, config.dry_run, &config.signer) { Ok(sig) => println!("Tx succeded (sig: {})", sig), Err(err) => println!("Error during bankruptcy handling:\n{:#?}", err), }; @@ -718,17 +736,17 @@ pub fn bank_update_emissions( rate: Option, additional_emissions: Option, ) -> Result<()> { - use crate::utils::calc_emissions_rate; - assert!(!(disable && (deposits || borrows))); + let rpc_client = config.mfi_program.rpc(); + let bank = config .mfi_program .account::(bank_pk) .unwrap_or_else(|_| panic!("Bank {} not found", bank_pk)); let emission_mint = bank.emissions_mint; - let funding_account_ata = get_associated_token_address(&config.payer.pubkey(), &emission_mint); + let funding_account_ata = get_associated_token_address(&config.signer.pubkey(), &emission_mint); let emissions_mint_decimals = config .mfi_program @@ -784,7 +802,7 @@ pub fn bank_update_emissions( program_id: marginfi::id(), accounts: marginfi::accounts::LendingPoolUpdateEmissionsParameters { marginfi_group: profile.marginfi_group.expect("marginfi group not set"), - admin: config.payer.pubkey(), + admin: config.signer.pubkey(), bank: bank_pk, emissions_mint: emission_mint, emissions_token_account: find_bank_emssions_token_account_pda( @@ -805,16 +823,17 @@ pub fn bank_update_emissions( .data(), }; - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&config.payer.pubkey()), - &[&config.payer], - config.mfi_program.rpc().get_latest_blockhash().unwrap(), - ); - - let rpc_program = config.mfi_program.rpc(); + let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); + let signing_keypairs = if let CliSigner::Keypair(keypair) = &config.signer { + vec![keypair] + } else { + vec![] + }; + let message = Message::new(&[ix], Some(&config.signer.pubkey())); + let mut transaction = Transaction::new_unsigned(message); + transaction.partial_sign(&signing_keypairs, recent_blockhash); - match process_transaction(&tx, &rpc_program, config.dry_run) { + match process_transaction(&transaction, &rpc_client, config.dry_run, &config.signer) { Ok(sig) => println!("Tx succeded (sig: {})", sig), Err(err) => println!("Error during bankruptcy handling:\n{:#?}", err), }; @@ -822,6 +841,53 @@ pub fn bank_update_emissions( Ok(()) } +#[cfg(feature = "admin")] +pub fn bank_configure( + config: Config, + profile: Profile, + bank_pk: Pubkey, + bank_config_opt: BankConfigOpt, +) -> Result<()> { + let rpc_client = config.mfi_program.rpc(); + + let mut configure_bank_ixs_builder = config.mfi_program.request(); + + let signing_keypairs = if let CliSigner::Keypair(keypair) = &config.signer { + configure_bank_ixs_builder = configure_bank_ixs_builder.signer(keypair); + vec![keypair] + } else { + vec![] + }; + + let mut configure_bank_ixs = configure_bank_ixs_builder + .accounts(marginfi::accounts::LendingPoolConfigureBank { + marginfi_group: profile.marginfi_group.unwrap(), + admin: config.signer.pubkey(), + bank: bank_pk, + }) + .args(marginfi::instruction::LendingPoolConfigureBank { + bank_config_opt: bank_config_opt.clone(), + }) + .instructions()?; + + if let Some(oracle) = &bank_config_opt.oracle { + configure_bank_ixs[0] + .accounts + .push(AccountMeta::new_readonly(oracle.keys[0], false)); + } + + let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); + let message = Message::new(&configure_bank_ixs, Some(&config.signer.pubkey())); + let mut transaction = Transaction::new_unsigned(message); + transaction.partial_sign(&signing_keypairs, recent_blockhash); + + let sig = process_transaction(&transaction, &rpc_client, config.dry_run, &config.signer)?; + + println!("Transaction signature: {}", sig); + + Ok(()) +} + // -------------------------------------------------------------------------------------------------------------------- // Profile // -------------------------------------------------------------------------------------------------------------------- @@ -830,7 +896,8 @@ pub fn bank_update_emissions( pub fn create_profile( name: String, cluster: Cluster, - keypair_path: String, + keypair_path: Option, + multisig: Option, rpc_url: String, program_id: Option, commitment: Option, @@ -842,6 +909,7 @@ pub fn create_profile( name, cluster, keypair_path, + multisig, rpc_url, program_id, commitment, @@ -946,6 +1014,7 @@ pub fn configure_profile( name: String, cluster: Option, keypair_path: Option, + multisig: Option, rpc_url: Option, program_id: Option, commitment: Option, @@ -956,6 +1025,7 @@ pub fn configure_profile( profile.config( cluster, keypair_path, + multisig, rpc_url, program_id, commitment, @@ -966,54 +1036,13 @@ pub fn configure_profile( Ok(()) } -#[cfg(feature = "admin")] -pub fn bank_configure( - config: Config, - profile: Profile, - bank_pk: Pubkey, - bank_config_opt: BankConfigOpt, -) -> Result<()> { - let mut configure_bank_ix = config - .mfi_program - .request() - .signer(&config.payer) - .accounts(marginfi::accounts::LendingPoolConfigureBank { - marginfi_group: profile.marginfi_group.unwrap(), - admin: config.payer.pubkey(), - bank: bank_pk, - }) - .args(marginfi::instruction::LendingPoolConfigureBank { - bank_config_opt: bank_config_opt.clone(), - }) - .instructions()?; - - if let Some(oracle) = &bank_config_opt.oracle { - configure_bank_ix[0] - .accounts - .push(AccountMeta::new_readonly(oracle.keys[0], false)); - } - - let transaction = Transaction::new_signed_with_payer( - &configure_bank_ix, - Some(&config.payer.pubkey()), - &[&config.payer], - config.mfi_program.rpc().get_latest_blockhash().unwrap(), - ); - - let sig = process_transaction(&transaction, &config.mfi_program.rpc(), config.dry_run)?; - - println!("Transaction signature: {}", sig); - - Ok(()) -} - // -------------------------------------------------------------------------------------------------------------------- // Marginfi Accounts // -------------------------------------------------------------------------------------------------------------------- pub fn marginfi_account_list(profile: Profile, config: &Config) -> Result<()> { let group = profile.marginfi_group.expect("Missing marginfi group"); - let authority = config.payer.pubkey(); + let authority = config.signer.pubkey(); let banks = HashMap::from_iter(load_all_banks(config, Some(group))?); @@ -1110,7 +1139,7 @@ pub fn marginfi_account_use( marginfi_account_pk: Pubkey, ) -> Result<()> { let group = profile.marginfi_group.expect("Missing marginfi group"); - let authority = config.payer.pubkey(); + let authority = config.signer.pubkey(); let marginfi_account = config .mfi_program @@ -1131,6 +1160,7 @@ pub fn marginfi_account_use( None, None, None, + None, Some(marginfi_account_pk), )?; @@ -1169,6 +1199,14 @@ pub fn marginfi_account_deposit( bank_pk: Pubkey, ui_amount: f64, ) -> Result<()> { + let signer = if let CliSigner::Keypair(signer) = &config.signer { + signer + } else { + bail!("Only keypair is supported for marginfi account actions"); + }; + + let rpc_client = config.mfi_program.rpc(); + let marginfi_account_pk = profile.get_marginfi_account(); let bank = config.mfi_program.account::(bank_pk)?; @@ -1182,17 +1220,15 @@ pub fn marginfi_account_deposit( bail!("Bank does not belong to group") } - let deposit_ata = anchor_spl::associated_token::get_associated_token_address( - &config.payer.pubkey(), - &bank.mint, - ); + let deposit_ata = + anchor_spl::associated_token::get_associated_token_address(&signer.pubkey(), &bank.mint); let ix = Instruction { program_id: config.program_id, accounts: marginfi::accounts::LendingAccountDeposit { marginfi_group: profile.marginfi_group.unwrap(), marginfi_account: marginfi_account_pk, - signer: config.payer.pubkey(), + signer: signer.pubkey(), bank: bank_pk, signer_token_account: deposit_ata, bank_liquidity_vault: bank.liquidity_vault, @@ -1202,14 +1238,15 @@ pub fn marginfi_account_deposit( data: marginfi::instruction::LendingAccountDeposit { amount }.data(), }; + let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); let tx = Transaction::new_signed_with_payer( &[ix], - Some(&config.payer.pubkey()), - &[&config.payer], - config.mfi_program.rpc().get_latest_blockhash()?, + Some(&signer.pubkey()), + &[signer], + recent_blockhash, ); - match process_transaction(&tx, &config.mfi_program.rpc(), config.dry_run) { + match process_transaction(&tx, &rpc_client, config.dry_run, &config.signer) { Ok(sig) => println!("Deposit successful: {sig}"), Err(err) => println!("Error during deposit:\n{err:#?}"), } @@ -1224,6 +1261,14 @@ pub fn marginfi_account_withdraw( ui_amount: f64, withdraw_all: bool, ) -> Result<()> { + let signer = if let CliSigner::Keypair(signer) = &config.signer { + signer + } else { + bail!("Only keypair is supported for marginfi account actions"); + }; + + let rpc_client = config.mfi_program.rpc(); + let marginfi_account_pk = profile.get_marginfi_account(); let banks = HashMap::from_iter(load_all_banks( @@ -1245,17 +1290,15 @@ pub fn marginfi_account_withdraw( bail!("Bank does not belong to group") } - let withdraw_ata = anchor_spl::associated_token::get_associated_token_address( - &config.payer.pubkey(), - &bank.mint, - ); + let withdraw_ata = + anchor_spl::associated_token::get_associated_token_address(&signer.pubkey(), &bank.mint); let mut ix = Instruction { program_id: config.program_id, accounts: marginfi::accounts::LendingAccountWithdraw { marginfi_group: profile.marginfi_group.unwrap(), marginfi_account: marginfi_account_pk, - signer: config.payer.pubkey(), + signer: signer.pubkey(), bank: bank_pk, bank_liquidity_vault: bank.liquidity_vault, token_program: token::ID, @@ -1283,20 +1326,21 @@ pub fn marginfi_account_withdraw( )); let create_ide_ata_ix = create_associated_token_account_idempotent( - &config.payer.pubkey(), - &config.payer.pubkey(), + &signer.pubkey(), + &signer.pubkey(), &bank.mint, &spl_token::ID, ); + let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); let tx = Transaction::new_signed_with_payer( &[create_ide_ata_ix, ix], - Some(&config.payer.pubkey()), - &[&config.payer], - config.mfi_program.rpc().get_latest_blockhash()?, + Some(&signer.pubkey()), + &[signer], + recent_blockhash, ); - match process_transaction(&tx, &config.mfi_program.rpc(), config.dry_run) { + match process_transaction(&tx, &rpc_client, config.dry_run, &config.signer) { Ok(sig) => println!("Withdraw successful: {sig}"), Err(err) => println!("Error during withdraw:\n{err:#?}"), } @@ -1310,6 +1354,14 @@ pub fn marginfi_account_borrow( bank_pk: Pubkey, ui_amount: f64, ) -> Result<()> { + let signer = if let CliSigner::Keypair(signer) = &config.signer { + signer + } else { + bail!("Only keypair is supported for marginfi account actions"); + }; + + let rpc_client = config.mfi_program.rpc(); + let marginfi_account_pk = profile.get_marginfi_account(); let banks = HashMap::from_iter(load_all_banks( @@ -1331,17 +1383,15 @@ pub fn marginfi_account_borrow( bail!("Bank does not belong to group") } - let withdraw_ata = anchor_spl::associated_token::get_associated_token_address( - &config.payer.pubkey(), - &bank.mint, - ); + let withdraw_ata = + anchor_spl::associated_token::get_associated_token_address(&signer.pubkey(), &bank.mint); let mut ix = Instruction { program_id: config.program_id, accounts: marginfi::accounts::LendingAccountBorrow { marginfi_group: profile.marginfi_group.unwrap(), marginfi_account: marginfi_account_pk, - signer: config.payer.pubkey(), + signer: signer.pubkey(), bank: bank_pk, bank_liquidity_vault: bank.liquidity_vault, token_program: token::ID, @@ -1365,20 +1415,21 @@ pub fn marginfi_account_borrow( )); let create_ide_ata_ix = create_associated_token_account_idempotent( - &config.payer.pubkey(), - &config.payer.pubkey(), + &signer.pubkey(), + &signer.pubkey(), &bank.mint, &spl_token::ID, ); + let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); let tx = Transaction::new_signed_with_payer( &[create_ide_ata_ix, ix], - Some(&config.payer.pubkey()), - &[&config.payer], - config.mfi_program.rpc().get_latest_blockhash()?, + Some(&signer.pubkey()), + &[signer], + recent_blockhash, ); - match process_transaction(&tx, &config.mfi_program.rpc(), config.dry_run) { + match process_transaction(&tx, &rpc_client, config.dry_run, &config.signer) { Ok(sig) => println!("Borrow successful: {sig}"), Err(err) => println!("Error during borrow:\n{err:#?}"), } @@ -1394,6 +1445,14 @@ pub fn marginfi_account_liquidate( liability_bank_pk: Pubkey, ui_asset_amount: f64, ) -> Result<()> { + let signer = if let CliSigner::Keypair(signer) = &config.signer { + signer + } else { + bail!("Only keypair is supported for marginfi account actions"); + }; + + let rpc_client = config.mfi_program.rpc(); + let marginfi_account_pk = profile.get_marginfi_account(); let banks = HashMap::from_iter(load_all_banks( @@ -1433,7 +1492,7 @@ pub fn marginfi_account_liquidate( asset_bank: asset_bank_pk, liab_bank: liability_bank_pk, liquidator_marginfi_account: marginfi_account_pk, - signer: config.payer.pubkey(), + signer: signer.pubkey(), liquidatee_marginfi_account: liquidatee_marginfi_account_pk, bank_liquidity_vault_authority: find_bank_vault_authority_pda( &liability_bank_pk, @@ -1474,14 +1533,20 @@ pub fn marginfi_account_liquidate( let cu_ix = ComputeBudgetInstruction::set_compute_unit_limit(1_400_000); + let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); let tx = Transaction::new_signed_with_payer( &[ix, cu_ix], - Some(&config.payer.pubkey()), - &[&config.payer], - config.mfi_program.rpc().get_latest_blockhash()?, + Some(&signer.pubkey()), + &[signer], + recent_blockhash, ); - match process_transaction(&tx, &config.mfi_program.rpc(), config.dry_run) { + match process_transaction( + &tx, + &config.mfi_program.rpc(), + config.dry_run, + &config.signer, + ) { Ok(sig) => println!("Liquidation successful: {sig}"), Err(err) => println!("Error during liquidation:\n{err:#?}"), } @@ -1490,6 +1555,14 @@ pub fn marginfi_account_liquidate( } pub fn marginfi_account_create(profile: &Profile, config: &Config) -> Result<()> { + let signer = if let CliSigner::Keypair(signer) = &config.signer { + signer + } else { + bail!("Only keypair is supported for marginfi account actions"); + }; + + let rpc_client = config.mfi_program.rpc(); + let marginfi_account_key = Keypair::new(); let ix = Instruction { @@ -1498,23 +1571,29 @@ pub fn marginfi_account_create(profile: &Profile, config: &Config) -> Result<()> marginfi_group: profile.marginfi_group.unwrap(), marginfi_account: marginfi_account_key.pubkey(), system_program: system_program::ID, - authority: config.payer.pubkey(), - fee_payer: config.payer.pubkey(), + authority: signer.pubkey(), + fee_payer: signer.pubkey(), } .to_account_metas(Some(true)), data: marginfi::instruction::MarginfiAccountInitialize.data(), }; + let recent_blockhash = rpc_client.get_latest_blockhash().unwrap(); let tx = Transaction::new_signed_with_payer( &[ix], - Some(&config.payer.pubkey()), - &[&config.payer, &marginfi_account_key], - config.mfi_program.rpc().get_latest_blockhash()?, + Some(&signer.pubkey()), + &[&signer, &marginfi_account_key], + recent_blockhash, ); let marginfi_account_pk = marginfi_account_key.pubkey(); - match process_transaction(&tx, &config.mfi_program.rpc(), config.dry_run) { + match process_transaction( + &tx, + &config.mfi_program.rpc(), + config.dry_run, + &config.signer, + ) { Ok(_sig) => print!("{marginfi_account_pk}"), Err(err) => println!("Error during initialize:\n{err:#?}"), } @@ -1528,6 +1607,7 @@ pub fn marginfi_account_create(profile: &Profile, config: &Config) -> Result<()> None, None, None, + None, Some(marginfi_account_key.pubkey()), )?; diff --git a/clients/rust/marginfi-cli/src/profile.rs b/clients/rust/marginfi-cli/src/profile.rs index 2568ce82..e3af0c13 100644 --- a/clients/rust/marginfi-cli/src/profile.rs +++ b/clients/rust/marginfi-cli/src/profile.rs @@ -1,24 +1,24 @@ -use crate::config::{Config, GlobalOptions}; -use anchor_client::{Client, Cluster}; -use anyhow::bail; -use anyhow::{anyhow, Result}; -use dirs::home_dir; -use serde::{Deserialize, Serialize}; -use solana_sdk::{ - commitment_config::{CommitmentConfig, CommitmentLevel}, - pubkey, - pubkey::Pubkey, - signature::read_keypair_file, - signer::Signer, +use { + crate::config::{CliSigner, Config, GlobalOptions}, + anchor_client::{Client, Cluster}, + anyhow::{anyhow, bail, Result}, + dirs::home_dir, + serde::{Deserialize, Serialize}, + solana_sdk::{ + commitment_config::{CommitmentConfig, CommitmentLevel}, + pubkey, + pubkey::Pubkey, + signature::{read_keypair_file, Keypair}, + }, + std::{fs, path::PathBuf, rc::Rc}, }; -use std::{fs, path::PathBuf, rc::Rc}; - #[derive(Serialize, Deserialize, Clone)] pub struct Profile { pub name: String, pub cluster: Cluster, - pub keypair_path: String, + pub keypair_path: Option, + pub multisig: Option, pub rpc_url: String, pub program_id: Option, pub commitment: Option, @@ -36,17 +36,27 @@ impl Profile { pub fn new( name: String, cluster: Cluster, - keypair_path: String, + keypair_path: Option, + multisig: Option, rpc_url: String, program_id: Option, commitment: Option, marginfi_group: Option, marginfi_account: Option, ) -> Self { + if keypair_path.is_none() && multisig.is_none() { + panic!("Either keypair_path or multisig must be set"); + } + + if keypair_path.is_some() && multisig.is_some() { + panic!("Only one of keypair_path or multisig can be set"); + } + Profile { name, cluster, keypair_path, + multisig, rpc_url, program_id, commitment, @@ -56,11 +66,16 @@ impl Profile { } pub fn get_config(&self, global_options: Option<&GlobalOptions>) -> Result { - let wallet_path = self.keypair_path.clone(); - let payer = read_keypair_file(&*shellexpand::tilde(&wallet_path)) - .expect("Example requires a keypair file"); - let payer_clone = read_keypair_file(&*shellexpand::tilde(&wallet_path)) - .expect("Example requires a keypair file"); + let signer = if let Some(keypair_path) = &self.keypair_path { + let wallet_path = keypair_path.clone(); + + CliSigner::Keypair( + read_keypair_file(&*shellexpand::tilde(&wallet_path)) + .expect("Example requires a keypair file"), + ) + } else { + CliSigner::Multisig(self.multisig.unwrap()) + }; let dry_run = match global_options { Some(options) => options.dry_run, @@ -83,7 +98,7 @@ impl Profile { }; let client = Client::new_with_options( Cluster::Custom(self.rpc_url.clone(), "https://dontcare.com:123".to_string()), - Rc::new(payer_clone), + Rc::new(Keypair::new()), commitment, ); let program = client.program(program_id); @@ -98,7 +113,7 @@ impl Profile { Ok(Config { cluster, - payer, + signer, program_id, commitment, dry_run, @@ -113,18 +128,29 @@ impl Profile { &mut self, cluster: Option, keypair_path: Option, + multisig: Option, rpc_url: Option, program_id: Option, commitment: Option, group: Option, account: Option, ) -> Result<()> { + if keypair_path.is_some() && multisig.is_some() { + panic!("Only one of keypair_path or multisig can be set"); + } + if let Some(cluster) = cluster { self.cluster = cluster; } if let Some(keypair_path) = keypair_path { - self.keypair_path = keypair_path; + self.keypair_path = Some(keypair_path); + self.multisig = None; + } + + if let Some(multisig) = multisig { + self.multisig = Some(multisig); + self.keypair_path = None; } if let Some(rpc_url) = rpc_url { @@ -240,6 +266,7 @@ Profile: Rpc URL: {} Signer: {} Keypair: {} + Multisig: {} "#, self.name, config.program_id, @@ -251,8 +278,13 @@ Profile: .unwrap_or_else(|| "None".to_owned()), self.cluster, self.rpc_url, - config.payer.pubkey(), - self.keypair_path, + config.signer.pubkey(), + self.keypair_path + .clone() + .unwrap_or_else(|| "None".to_owned()), + self.multisig + .map(|x| x.to_string()) + .unwrap_or_else(|| "None".to_owned()), )?; Ok(()) diff --git a/clients/rust/marginfi-cli/src/utils.rs b/clients/rust/marginfi-cli/src/utils.rs index 1423b735..08846078 100644 --- a/clients/rust/marginfi-cli/src/utils.rs +++ b/clients/rust/marginfi-cli/src/utils.rs @@ -1,27 +1,34 @@ -use anyhow::{bail, Result}; -use fixed::types::I80F48; -use fixed_macro::types::I80F48; -use log::error; -use marginfi::{ - bank_authority_seed, - constants::{EMISSIONS_AUTH_SEED, EMISSIONS_TOKEN_ACCOUNT_SEED}, - state::{ - marginfi_account::MarginfiAccount, - marginfi_group::{Bank, BankVaultType}, +use { + crate::config::CliSigner, + anyhow::{bail, Result}, + fixed::types::I80F48, + fixed_macro::types::I80F48, + log::error, + marginfi::{ + bank_authority_seed, + state::{ + marginfi_account::MarginfiAccount, + marginfi_group::{Bank, BankVaultType}, + }, + }, + solana_client::rpc_client::RpcClient, + solana_sdk::{ + instruction::AccountMeta, pubkey::Pubkey, signature::Signature, transaction::Transaction, }, + std::collections::HashMap, }; + #[cfg(feature = "admin")] -use marginfi::{bank_seed, constants::MAX_ORACLE_KEYS}; -use solana_client::rpc_client::RpcClient; -use solana_sdk::{ - instruction::AccountMeta, pubkey::Pubkey, signature::Signature, transaction::Transaction, +use marginfi::{ + bank_seed, + constants::{EMISSIONS_AUTH_SEED, EMISSIONS_TOKEN_ACCOUNT_SEED, MAX_ORACLE_KEYS}, }; -use std::collections::HashMap; pub fn process_transaction( tx: &Transaction, rpc_client: &RpcClient, dry_run: bool, + signer: &CliSigner, ) -> Result { if dry_run { match rpc_client.simulate_transaction(tx) { @@ -38,6 +45,14 @@ pub fn process_transaction( } Err(err) => bail!(err), } + } else if let CliSigner::Multisig(_) = signer { + let tx_serialized = bs58::encode(bincode::serialize(tx)?).into_string(); + + println!("------- transaction -------"); + println!("{}", tx_serialized); + println!("---------------------------"); + + Ok(Signature::default()) } else { match rpc_client.send_and_confirm_transaction_with_spinner(tx) { Ok(sig) => Ok(sig), @@ -66,6 +81,7 @@ pub fn find_bank_vault_authority_pda( Pubkey::find_program_address(bank_authority_seed!(vault_type, bank_pk), program_id) } +#[cfg(feature = "admin")] pub fn find_bank_emssions_auth_pda( bank: Pubkey, emissions_mint: Pubkey, @@ -81,6 +97,7 @@ pub fn find_bank_emssions_auth_pda( ) } +#[cfg(feature = "admin")] pub fn find_bank_emssions_token_account_pda( bank: Pubkey, emissions_mint: Pubkey, diff --git a/test-utils/src/bank.rs b/test-utils/src/bank.rs index 6e032627..c8d6690c 100644 --- a/test-utils/src/bank.rs +++ b/test-utils/src/bank.rs @@ -277,7 +277,7 @@ impl BankFixture { .await .unwrap() .unwrap(); - let mut bank = bytemuck::from_bytes_mut::(&mut bank_ai.data.as_mut_slice()[8..]); + let bank = bytemuck::from_bytes_mut::(&mut bank_ai.data.as_mut_slice()[8..]); bank.asset_share_value = value.into();