Merge pull request #147 from blockworks-foundation/ckamm/liq-and-rebalance
Liquidate and buy/sell to keep only quote
This commit is contained in:
commit
fbdc5ee6b4
|
@ -1276,6 +1276,7 @@ dependencies = [
|
|||
"solana-account-decoder",
|
||||
"solana-client",
|
||||
"solana-sdk",
|
||||
"spl-associated-token-account",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
|
|
@ -176,7 +176,13 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
let input_mint = client::pubkey_from_cli(&cmd.input_mint);
|
||||
let output_mint = client::pubkey_from_cli(&cmd.output_mint);
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner)?;
|
||||
let txsig = client.jupiter_swap(input_mint, output_mint, cmd.amount, cmd.slippage)?;
|
||||
let txsig = client.jupiter_swap(
|
||||
input_mint,
|
||||
output_mint,
|
||||
cmd.amount,
|
||||
cmd.slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::GroupAddress { creator, num } => {
|
||||
|
|
|
@ -21,6 +21,7 @@ shellexpand = "2.1.0"
|
|||
solana-account-decoder = "~1.10.29"
|
||||
solana-client = "~1.10.29"
|
||||
solana-sdk = "~1.10.29"
|
||||
spl-associated-token-account = "1.0.3"
|
||||
thiserror = "1.0.31"
|
||||
log = "0.4"
|
||||
reqwest = "0.11.11"
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::chain_data::*;
|
||||
|
||||
|
@ -11,7 +13,9 @@ use anyhow::Context;
|
|||
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::account::{AccountSharedData, ReadableAccount};
|
||||
use solana_sdk::clock::Slot;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Signature;
|
||||
|
||||
pub struct AccountFetcher {
|
||||
pub chain_data: Arc<RwLock<ChainData>>,
|
||||
|
@ -66,11 +70,12 @@ impl AccountFetcher {
|
|||
.clone())
|
||||
}
|
||||
|
||||
pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<()> {
|
||||
pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
|
||||
let response = self
|
||||
.rpc
|
||||
.get_account_with_commitment(&address, self.rpc.commitment())
|
||||
.with_context(|| format!("refresh account {} via rpc", address))?;
|
||||
let slot = response.context.slot;
|
||||
let account = response
|
||||
.value
|
||||
.ok_or(anchor_client::ClientError::AccountNotFound)
|
||||
|
@ -85,6 +90,43 @@ impl AccountFetcher {
|
|||
},
|
||||
);
|
||||
|
||||
Ok(slot)
|
||||
}
|
||||
|
||||
/// Return the maximum slot reported for the processing of the signatures
|
||||
pub fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result<Slot> {
|
||||
let statuses = self.rpc.get_signature_statuses(signatures)?.value;
|
||||
Ok(statuses
|
||||
.iter()
|
||||
.map(|status_opt| status_opt.as_ref().map(|status| status.slot).unwrap_or(0))
|
||||
.max()
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Return success once all addresses have data >= min_slot
|
||||
pub fn refresh_accounts_via_rpc_until_slot(
|
||||
&self,
|
||||
addresses: &[Pubkey],
|
||||
min_slot: Slot,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
let start = Instant::now();
|
||||
for address in addresses {
|
||||
loop {
|
||||
if start.elapsed() > timeout {
|
||||
anyhow::bail!(
|
||||
"timeout while waiting for data for {} that's newer than slot {}",
|
||||
address,
|
||||
min_slot
|
||||
);
|
||||
}
|
||||
let data_slot = self.refresh_account_via_rpc(address)?;
|
||||
if data_slot >= min_slot {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -395,6 +395,57 @@ impl MangoClient {
|
|||
.map_err(prettify_client_error)
|
||||
}
|
||||
|
||||
pub fn token_withdraw(
|
||||
&self,
|
||||
mint: Pubkey,
|
||||
amount: u64,
|
||||
allow_borrow: bool,
|
||||
) -> anyhow::Result<Signature> {
|
||||
let token = self.context.token_by_mint(&mint)?;
|
||||
let token_index = token.token_index;
|
||||
let mint_info = token.mint_info;
|
||||
|
||||
let health_check_metas =
|
||||
self.derive_health_check_remaining_account_metas(vec![token_index], false)?;
|
||||
|
||||
self.program()
|
||||
.request()
|
||||
.instruction(create_associated_token_account_idempotent(
|
||||
&self.owner(),
|
||||
&self.owner(),
|
||||
&mint,
|
||||
))
|
||||
.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::TokenWithdraw {
|
||||
group: self.group(),
|
||||
account: self.mango_account_address,
|
||||
owner: self.owner(),
|
||||
bank: mint_info.first_bank(),
|
||||
vault: mint_info.first_vault(),
|
||||
token_account: get_associated_token_address(
|
||||
&self.owner(),
|
||||
&mint_info.mint,
|
||||
),
|
||||
token_program: Token::id(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
ams.extend(health_check_metas.into_iter());
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenWithdraw {
|
||||
amount,
|
||||
allow_borrow,
|
||||
}),
|
||||
})
|
||||
.signer(&self.owner)
|
||||
.send()
|
||||
.map_err(prettify_client_error)
|
||||
}
|
||||
|
||||
pub fn get_oracle_price(
|
||||
&self,
|
||||
token_name: &str,
|
||||
|
@ -837,33 +888,50 @@ impl MangoClient {
|
|||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
source_amount: u64,
|
||||
amount: u64,
|
||||
slippage: f64,
|
||||
swap_mode: JupiterSwapMode,
|
||||
) -> anyhow::Result<Signature> {
|
||||
self.invoke(self.jupiter_swap_async(input_mint, output_mint, source_amount, slippage))
|
||||
self.invoke(self.jupiter_swap_async(input_mint, output_mint, amount, slippage, swap_mode))
|
||||
}
|
||||
|
||||
// Not actually fully async, since it uses the blocking RPC client to send the actual tx
|
||||
pub async fn jupiter_swap_async(
|
||||
pub fn jupiter_route(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
source_amount: u64,
|
||||
amount: u64,
|
||||
slippage: f64,
|
||||
) -> anyhow::Result<Signature> {
|
||||
let source_token = self.context.token_by_mint(&input_mint)?;
|
||||
let target_token = self.context.token_by_mint(&output_mint)?;
|
||||
swap_mode: JupiterSwapMode,
|
||||
) -> anyhow::Result<jupiter::QueryRoute> {
|
||||
self.invoke(self.jupiter_route_async(input_mint, output_mint, amount, slippage, swap_mode))
|
||||
}
|
||||
|
||||
pub async fn jupiter_route_async(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
slippage: f64,
|
||||
swap_mode: JupiterSwapMode,
|
||||
) -> anyhow::Result<jupiter::QueryRoute> {
|
||||
let quote = self
|
||||
.http_client
|
||||
.get("https://quote-api.jup.ag/v1/quote")
|
||||
.query(&[
|
||||
("inputMint", input_mint.to_string()),
|
||||
("outputMint", output_mint.to_string()),
|
||||
("amount", format!("{}", source_amount)),
|
||||
("amount", format!("{}", amount)),
|
||||
("onlyDirectRoutes", "true".into()),
|
||||
("filterTopNResult", "10".into()),
|
||||
("slippage", format!("{}", slippage)),
|
||||
(
|
||||
"swapMode",
|
||||
match swap_mode {
|
||||
JupiterSwapMode::ExactIn => "ExactIn",
|
||||
JupiterSwapMode::ExactOut => "ExactOut",
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
|
@ -889,6 +957,23 @@ impl MangoClient {
|
|||
)
|
||||
})?;
|
||||
|
||||
Ok(route.clone())
|
||||
}
|
||||
|
||||
pub async fn jupiter_swap_async(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
slippage: f64,
|
||||
swap_mode: JupiterSwapMode,
|
||||
) -> anyhow::Result<Signature> {
|
||||
let source_token = self.context.token_by_mint(&input_mint)?;
|
||||
let target_token = self.context.token_by_mint(&output_mint)?;
|
||||
let route = self
|
||||
.jupiter_route_async(input_mint, output_mint, amount, slippage, swap_mode)
|
||||
.await?;
|
||||
|
||||
let swap = self
|
||||
.http_client
|
||||
.post("https://quote-api.jup.ag/v1/swap")
|
||||
|
@ -919,19 +1004,12 @@ impl MangoClient {
|
|||
.context("base64 decoding jupiter transaction")?,
|
||||
)
|
||||
.context("parsing jupiter transaction")?;
|
||||
let ata_program = anchor_spl::associated_token::ID;
|
||||
let token_program = anchor_spl::token::ID;
|
||||
let is_setup_ix = |k: Pubkey| -> bool { k == ata_program || k == token_program };
|
||||
let jup_ixs = deserialize_instructions(&jup_tx.message)
|
||||
.into_iter()
|
||||
// TODO: possibly creating associated token accounts if they don't exist yet is good?!
|
||||
// we could squeeze the FlashLoan instructions in the middle:
|
||||
// - beginning AToken...
|
||||
// - FlashLoanBegin
|
||||
// - other JUP ix
|
||||
// - FlashLoanEnd
|
||||
// - ending AToken
|
||||
.filter(|ix| {
|
||||
ix.program_id
|
||||
!= Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap()
|
||||
})
|
||||
.filter(|ix| !is_setup_ix(ix.program_id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let bank_ams = [
|
||||
|
@ -962,7 +1040,14 @@ impl MangoClient {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let loan_amounts = vec![source_amount, 0u64];
|
||||
let loan_amounts = vec![
|
||||
match swap_mode {
|
||||
JupiterSwapMode::ExactIn => amount,
|
||||
// in amount + slippage
|
||||
JupiterSwapMode::ExactOut => route.other_amount_threshold,
|
||||
},
|
||||
0u64,
|
||||
];
|
||||
|
||||
// This relies on the fact that health account banks will be identical to the first_bank above!
|
||||
let health_ams = self
|
||||
|
@ -973,7 +1058,19 @@ impl MangoClient {
|
|||
.context("building health accounts")?;
|
||||
|
||||
let program = self.program();
|
||||
let mut builder = program.request().instruction(Instruction {
|
||||
let mut builder = program.request();
|
||||
|
||||
builder = builder.instruction(create_associated_token_account_idempotent(
|
||||
&self.owner.pubkey(),
|
||||
&self.owner.pubkey(),
|
||||
&source_token.mint_info.mint,
|
||||
));
|
||||
builder = builder.instruction(create_associated_token_account_idempotent(
|
||||
&self.owner.pubkey(),
|
||||
&self.owner.pubkey(),
|
||||
&target_token.mint_info.mint,
|
||||
));
|
||||
builder = builder.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
|
@ -994,7 +1091,7 @@ impl MangoClient {
|
|||
}),
|
||||
});
|
||||
for ix in jup_ixs {
|
||||
builder = builder.instruction(ix);
|
||||
builder = builder.instruction(ix.clone());
|
||||
}
|
||||
builder = builder.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
|
@ -1094,6 +1191,12 @@ pub fn prettify_client_error(err: anchor_client::ClientError) -> anyhow::Error {
|
|||
err.into()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum JupiterSwapMode {
|
||||
ExactIn,
|
||||
ExactOut,
|
||||
}
|
||||
|
||||
pub fn keypair_from_cli(keypair: &str) -> Keypair {
|
||||
let maybe_keypair = keypair::read_keypair(&mut keypair.as_bytes());
|
||||
match maybe_keypair {
|
||||
|
@ -1128,3 +1231,17 @@ fn to_writable_account_meta(pubkey: Pubkey) -> AccountMeta {
|
|||
is_signer: false,
|
||||
}
|
||||
}
|
||||
|
||||
// FUTURE: use spl_associated_token_account::instruction::create_associated_token_account_idempotent
|
||||
// Right now anchor depends on an earlier version of this package...
|
||||
fn create_associated_token_account_idempotent(
|
||||
funder: &Pubkey,
|
||||
owner: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
) -> Instruction {
|
||||
let mut instr = spl_associated_token_account::instruction::create_associated_token_account(
|
||||
funder, owner, mint,
|
||||
);
|
||||
instr.data = vec![0x1]; // CreateIdempotent
|
||||
instr
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ pub struct QueryRoute {
|
|||
pub out_amount: u64,
|
||||
pub amount: u64,
|
||||
pub other_amount_threshold: u64,
|
||||
pub out_amount_with_slippage: u64,
|
||||
pub out_amount_with_slippage: Option<u64>,
|
||||
pub swap_mode: String,
|
||||
pub price_impact_pct: f64,
|
||||
pub market_infos: Vec<QueryMarketInfo>,
|
||||
|
@ -33,7 +33,7 @@ pub struct QueryMarketInfo {
|
|||
pub lp_fee: QueryFee,
|
||||
pub platform_fee: QueryFee,
|
||||
pub not_enough_liquidity: bool,
|
||||
pub price_impact_pct: f64,
|
||||
pub price_impact_pct: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
|
@ -41,7 +41,7 @@ pub struct QueryMarketInfo {
|
|||
pub struct QueryFee {
|
||||
pub amount: u64,
|
||||
pub mint: String,
|
||||
pub pct: f64,
|
||||
pub pct: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
|
@ -49,6 +49,7 @@ pub struct QueryFee {
|
|||
pub struct SwapRequest {
|
||||
pub route: QueryRoute,
|
||||
pub user_public_key: String,
|
||||
#[serde(rename = "wrapUnwrapSOL")]
|
||||
pub wrap_unwrap_sol: bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,15 @@
|
|||
use solana_sdk::signature::Keypair;
|
||||
use solana_client::{
|
||||
client_error::Result as ClientResult, rpc_client::RpcClient, rpc_request::RpcError,
|
||||
};
|
||||
use solana_sdk::transaction::Transaction;
|
||||
use solana_sdk::{
|
||||
clock::Slot,
|
||||
commitment_config::CommitmentConfig,
|
||||
signature::{Keypair, Signature},
|
||||
transaction::uses_durable_nonce,
|
||||
};
|
||||
|
||||
use std::{thread, time};
|
||||
|
||||
// #[allow(dead_code)]
|
||||
// pub fn retry<T>(request: impl Fn() -> Result<T, anchor_client::ClientError>) -> anyhow::Result<T> {
|
||||
|
@ -24,3 +35,64 @@ impl MyClone for Keypair {
|
|||
Self::from_bytes(&self.to_bytes()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of RpcClient::send_and_confirm_transaction that returns the slot the
|
||||
/// transaction confirmed in.
|
||||
pub fn send_and_confirm_transaction(
|
||||
rpc_client: &RpcClient,
|
||||
transaction: &Transaction,
|
||||
) -> ClientResult<(Signature, Slot)> {
|
||||
const SEND_RETRIES: usize = 1;
|
||||
const GET_STATUS_RETRIES: usize = usize::MAX;
|
||||
|
||||
'sending: for _ in 0..SEND_RETRIES {
|
||||
let signature = rpc_client.send_transaction(transaction)?;
|
||||
|
||||
let recent_blockhash = if uses_durable_nonce(transaction).is_some() {
|
||||
let (recent_blockhash, ..) =
|
||||
rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::processed())?;
|
||||
recent_blockhash
|
||||
} else {
|
||||
transaction.message.recent_blockhash
|
||||
};
|
||||
|
||||
for status_retry in 0..GET_STATUS_RETRIES {
|
||||
let response = rpc_client.get_signature_statuses(&[signature])?.value;
|
||||
match response[0]
|
||||
.clone()
|
||||
.filter(|result| result.satisfies_commitment(rpc_client.commitment()))
|
||||
{
|
||||
Some(tx_status) => {
|
||||
return if let Some(e) = tx_status.err {
|
||||
Err(e.into())
|
||||
} else {
|
||||
Ok((signature, tx_status.slot))
|
||||
};
|
||||
}
|
||||
None => {
|
||||
if !rpc_client
|
||||
.is_blockhash_valid(&recent_blockhash, CommitmentConfig::processed())?
|
||||
{
|
||||
// Block hash is not found by some reason
|
||||
break 'sending;
|
||||
} else if cfg!(not(test))
|
||||
// Ignore sleep at last step.
|
||||
&& status_retry < GET_STATUS_RETRIES
|
||||
{
|
||||
// Retry twice a second
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(RpcError::ForUser(
|
||||
"unable to confirm transaction. \
|
||||
This can happen in situations such as transaction expiration \
|
||||
and insufficient fee-payer funds"
|
||||
.to_string(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::account_shared_data::KeyedAccountSharedData;
|
||||
|
||||
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
|
||||
use mango_v4::state::{
|
||||
new_health_cache, oracle_price, Bank, FixedOrderAccountRetriever, HealthCache, HealthType,
|
||||
MangoAccountValue, TokenIndex,
|
||||
MangoAccountValue, TokenIndex, QUOTE_TOKEN_INDEX,
|
||||
};
|
||||
|
||||
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
pub struct Config {
|
||||
pub min_health_ratio: f64,
|
||||
pub refresh_timeout: Duration,
|
||||
}
|
||||
|
||||
pub fn new_health_cache_(
|
||||
context: &MangoGroupContext,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
|
@ -36,14 +43,68 @@ pub fn new_health_cache_(
|
|||
new_health_cache(&account.borrow(), &retriever).context("make health cache")
|
||||
}
|
||||
|
||||
pub fn jupiter_market_can_buy(
|
||||
mango_client: &MangoClient,
|
||||
token: TokenIndex,
|
||||
quote_token: TokenIndex,
|
||||
) -> bool {
|
||||
if token == quote_token {
|
||||
return true;
|
||||
}
|
||||
let token_mint = mango_client.context.token(token).mint_info.mint;
|
||||
let quote_token_mint = mango_client.context.token(quote_token).mint_info.mint;
|
||||
|
||||
// Consider a market alive if we can swap $10 worth at 1% slippage
|
||||
// TODO: configurable
|
||||
// TODO: cache this, no need to recheck often
|
||||
let quote_amount = 10_000_000u64;
|
||||
let slippage = 1.0;
|
||||
mango_client
|
||||
.jupiter_route(
|
||||
quote_token_mint,
|
||||
token_mint,
|
||||
quote_amount,
|
||||
slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn jupiter_market_can_sell(
|
||||
mango_client: &MangoClient,
|
||||
token: TokenIndex,
|
||||
quote_token: TokenIndex,
|
||||
) -> bool {
|
||||
if token == quote_token {
|
||||
return true;
|
||||
}
|
||||
let token_mint = mango_client.context.token(token).mint_info.mint;
|
||||
let quote_token_mint = mango_client.context.token(quote_token).mint_info.mint;
|
||||
|
||||
// Consider a market alive if we can swap $10 worth at 1% slippage
|
||||
// TODO: configurable
|
||||
// TODO: cache this, no need to recheck often
|
||||
let quote_amount = 10_000_000u64;
|
||||
let slippage = 1.0;
|
||||
mango_client
|
||||
.jupiter_route(
|
||||
token_mint,
|
||||
quote_token_mint,
|
||||
quote_amount,
|
||||
slippage,
|
||||
client::JupiterSwapMode::ExactOut,
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn process_account(
|
||||
pub fn maybe_liquidate_account(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
pubkey: &Pubkey,
|
||||
) -> anyhow::Result<()> {
|
||||
// TODO: configurable
|
||||
let min_health_ratio = I80F48::from_num(50.0f64);
|
||||
config: &Config,
|
||||
) -> anyhow::Result<bool> {
|
||||
let min_health_ratio = I80F48::from_num(config.min_health_ratio);
|
||||
let quote_token_index = 0;
|
||||
|
||||
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
||||
|
@ -52,7 +113,7 @@ pub fn process_account(
|
|||
.health(HealthType::Maint);
|
||||
|
||||
if maint_health >= 0 && !account.is_bankrupt() {
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
|
@ -85,11 +146,11 @@ pub fn process_account(
|
|||
)?;
|
||||
Ok((
|
||||
token_position.token_index,
|
||||
bank,
|
||||
price,
|
||||
token_position.native(&bank) * price,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<(TokenIndex, Bank, I80F48)>>>()?;
|
||||
.collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
|
||||
tokens.sort_by(|a, b| a.2.cmp(&b.2));
|
||||
|
||||
let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
|
||||
|
@ -111,31 +172,69 @@ pub fn process_account(
|
|||
};
|
||||
|
||||
// try liquidating
|
||||
if account.is_bankrupt() {
|
||||
let txsig = if account.is_bankrupt() {
|
||||
if tokens.is_empty() {
|
||||
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey);
|
||||
}
|
||||
let (liab_token_index, _liab_bank, _liab_price) = tokens.first().unwrap();
|
||||
let liab_token_index = tokens
|
||||
.iter()
|
||||
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
||||
liab_usdc_equivalent.is_negative()
|
||||
&& jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
|
||||
pubkey,
|
||||
tokens
|
||||
)
|
||||
})?
|
||||
.0;
|
||||
|
||||
let max_liab_transfer = get_max_liab_transfer(*liab_token_index, quote_token_index)?;
|
||||
let max_liab_transfer = get_max_liab_transfer(liab_token_index, quote_token_index)?;
|
||||
|
||||
let sig = mango_client
|
||||
.liq_token_bankruptcy((pubkey, &account), *liab_token_index, max_liab_transfer)
|
||||
.liq_token_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer)
|
||||
.context("sending liq_token_bankruptcy")?;
|
||||
log::info!(
|
||||
"Liquidated bankruptcy for {}..., maint_health was {}, tx sig {:?}",
|
||||
&pubkey.to_string()[..3],
|
||||
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
|
||||
pubkey,
|
||||
maint_health,
|
||||
sig
|
||||
);
|
||||
sig
|
||||
} else if maint_health.is_negative() {
|
||||
if tokens.len() < 2 {
|
||||
anyhow::bail!("mango account {}, has less than 2 active tokens", pubkey);
|
||||
}
|
||||
let (asset_token_index, _asset_bank, _asset_price) = tokens.last().unwrap();
|
||||
let (liab_token_index, _liab_bank, _liab_price) = tokens.first().unwrap();
|
||||
let asset_token_index = tokens
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| {
|
||||
asset_usdc_equivalent.is_positive()
|
||||
&& jupiter_market_can_sell(mango_client, *asset_token_index, QUOTE_TOKEN_INDEX)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"mango account {}, has no asset tokens that are sellable for USDC: {:?}",
|
||||
pubkey,
|
||||
tokens
|
||||
)
|
||||
})?
|
||||
.0;
|
||||
let liab_token_index = tokens
|
||||
.iter()
|
||||
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
||||
liab_usdc_equivalent.is_negative()
|
||||
&& jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
|
||||
pubkey,
|
||||
tokens
|
||||
)
|
||||
})?
|
||||
.0;
|
||||
|
||||
let max_liab_transfer = get_max_liab_transfer(*liab_token_index, *asset_token_index)
|
||||
let max_liab_transfer = get_max_liab_transfer(liab_token_index, asset_token_index)
|
||||
.context("getting max_liab_transfer")?;
|
||||
|
||||
//
|
||||
|
@ -146,29 +245,43 @@ pub fn process_account(
|
|||
let sig = mango_client
|
||||
.liq_token_with_token(
|
||||
(pubkey, &account),
|
||||
*asset_token_index,
|
||||
*liab_token_index,
|
||||
asset_token_index,
|
||||
liab_token_index,
|
||||
max_liab_transfer,
|
||||
)
|
||||
.context("sending liq_token_with_token")?;
|
||||
log::info!(
|
||||
"Liquidated token with token for {}..., maint_health was {}, tx sig {:?}",
|
||||
&pubkey.to_string()[..3],
|
||||
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
|
||||
pubkey,
|
||||
maint_health,
|
||||
sig
|
||||
);
|
||||
sig
|
||||
} else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let slot = account_fetcher.transaction_max_slot(&[txsig])?;
|
||||
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
|
||||
&[*pubkey, mango_client.mango_account_address],
|
||||
slot,
|
||||
config.refresh_timeout,
|
||||
) {
|
||||
log::info!("could not refresh after liquidation: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn process_accounts<'a>(
|
||||
pub fn maybe_liquidate_one<'a>(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
accounts: impl Iterator<Item = &'a Pubkey>,
|
||||
) -> anyhow::Result<()> {
|
||||
config: &Config,
|
||||
) -> bool {
|
||||
for pubkey in accounts {
|
||||
match process_account(mango_client, account_fetcher, pubkey) {
|
||||
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config) {
|
||||
Err(err) => {
|
||||
// Not all errors need to be raised to the user's attention.
|
||||
let mut log_level = log::Level::Error;
|
||||
|
@ -185,9 +298,10 @@ pub fn process_accounts<'a>(
|
|||
};
|
||||
log::log!(log_level, "liquidating account {}: {:?}", pubkey, err);
|
||||
}
|
||||
Ok(true) => return true,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
false
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ use std::collections::HashSet;
|
|||
pub mod account_shared_data;
|
||||
pub mod liquidate;
|
||||
pub mod metrics;
|
||||
pub mod rebalance;
|
||||
pub mod snapshot_source;
|
||||
pub mod util;
|
||||
pub mod websocket_source;
|
||||
|
@ -67,13 +68,20 @@ struct Cli {
|
|||
#[clap(long, env, default_value = "300")]
|
||||
snapshot_interval_secs: u64,
|
||||
|
||||
// how many getMultipleAccounts requests to send in parallel
|
||||
/// how many getMultipleAccounts requests to send in parallel
|
||||
#[clap(long, env, default_value = "10")]
|
||||
parallel_rpc_requests: usize,
|
||||
|
||||
// typically 100 is the max number for getMultipleAccounts
|
||||
/// typically 100 is the max number of accounts getMultipleAccounts will retrieve at once
|
||||
#[clap(long, env, default_value = "100")]
|
||||
get_multiple_accounts_count: usize,
|
||||
|
||||
/// liquidator health ratio should not fall below this value
|
||||
#[clap(long, env, default_value = "50")]
|
||||
min_health_ratio: f64,
|
||||
|
||||
#[clap(long, env, default_value = "1")]
|
||||
rebalance_slippage: f64,
|
||||
}
|
||||
|
||||
pub fn encode_address(addr: &Pubkey) -> String {
|
||||
|
@ -114,8 +122,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
let group_context = MangoGroupContext::new_from_rpc(mango_group, cluster.clone(), commitment)?;
|
||||
|
||||
// TODO: this is all oracles, not just pyth!
|
||||
let mango_pyth_oracles = group_context
|
||||
let mango_oracles = group_context
|
||||
.tokens
|
||||
.values()
|
||||
.map(|value| value.mint_info.oracle)
|
||||
|
@ -145,7 +152,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
serum_program: cli.serum_program,
|
||||
open_orders_authority: mango_group,
|
||||
},
|
||||
mango_pyth_oracles.clone(),
|
||||
mango_oracles.clone(),
|
||||
websocket_sender,
|
||||
);
|
||||
|
||||
|
@ -169,7 +176,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
snapshot_interval: std::time::Duration::from_secs(cli.snapshot_interval_secs),
|
||||
min_slot: first_websocket_slot + 10,
|
||||
},
|
||||
mango_pyth_oracles,
|
||||
mango_oracles,
|
||||
snapshot_sender,
|
||||
);
|
||||
|
||||
|
@ -183,14 +190,6 @@ async fn main() -> anyhow::Result<()> {
|
|||
let mut oracles = HashSet::<Pubkey>::new();
|
||||
let mut perp_markets = HashMap::<PerpMarketIndex, Pubkey>::new();
|
||||
|
||||
// List of accounts that are potentially liquidatable.
|
||||
//
|
||||
// Used to send a different message for newly liqudatable accounts and
|
||||
// accounts that are still liquidatable but not fresh anymore.
|
||||
//
|
||||
// This should actually be done per connected websocket client, and not globally.
|
||||
let _current_candidates = HashSet::<Pubkey>::new();
|
||||
|
||||
// Is the first snapshot done? Only start checking account health when it is.
|
||||
let mut one_snapshot_done = false;
|
||||
|
||||
|
@ -211,6 +210,19 @@ async fn main() -> anyhow::Result<()> {
|
|||
)?)
|
||||
};
|
||||
|
||||
let liq_config = liquidate::Config {
|
||||
min_health_ratio: cli.min_health_ratio,
|
||||
// TODO: config
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
};
|
||||
|
||||
let mut rebalance_interval = tokio::time::interval(Duration::from_secs(5));
|
||||
let rebalance_config = rebalance::Config {
|
||||
slippage: cli.rebalance_slippage,
|
||||
// TODO: config
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
};
|
||||
|
||||
info!("main loop");
|
||||
loop {
|
||||
tokio::select! {
|
||||
|
@ -238,14 +250,13 @@ async fn main() -> anyhow::Result<()> {
|
|||
continue;
|
||||
}
|
||||
|
||||
if let Err(err) = liquidate::process_accounts(
|
||||
&mango_client,
|
||||
&account_fetcher,
|
||||
std::iter::once(&account_write.pubkey),
|
||||
|
||||
) {
|
||||
warn!("could not process account {}: {:?}", account_write.pubkey, err);
|
||||
}
|
||||
liquidate(
|
||||
&mango_client,
|
||||
&account_fetcher,
|
||||
std::iter::once(&account_write.pubkey),
|
||||
&liq_config,
|
||||
&rebalance_config,
|
||||
)?;
|
||||
}
|
||||
|
||||
if is_mango_bank(&account_write.account, &mango_program, &mango_group).is_some() || oracles.contains(&account_write.pubkey) {
|
||||
|
@ -261,21 +272,13 @@ async fn main() -> anyhow::Result<()> {
|
|||
log::debug!("change to oracle {}", &account_write.pubkey);
|
||||
}
|
||||
|
||||
// check health of all accounts
|
||||
//
|
||||
// TODO: This could be done asynchronously by calling
|
||||
// let accounts = chain_data.accounts_snapshot();
|
||||
// and then working with the snapshot of the data
|
||||
//
|
||||
// However, this currently takes like 50ms for me in release builds,
|
||||
// so optimizing much seems unnecessary.
|
||||
if let Err(err) = liquidate::process_accounts(
|
||||
&mango_client,
|
||||
&account_fetcher,
|
||||
mango_accounts.iter(),
|
||||
) {
|
||||
warn!("could not process accounts: {:?}", err);
|
||||
}
|
||||
liquidate(
|
||||
&mango_client,
|
||||
&account_fetcher,
|
||||
mango_accounts.iter(),
|
||||
&liq_config,
|
||||
&rebalance_config,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -302,19 +305,46 @@ async fn main() -> anyhow::Result<()> {
|
|||
snapshot_source::update_chain_data(&mut chain_data.write().unwrap(), message);
|
||||
one_snapshot_done = true;
|
||||
|
||||
// trigger a full health check
|
||||
if let Err(err) = liquidate::process_accounts(
|
||||
&mango_client,
|
||||
&account_fetcher,
|
||||
mango_accounts.iter(),
|
||||
) {
|
||||
warn!("could not process accounts: {:?}", err);
|
||||
}
|
||||
liquidate(
|
||||
&mango_client,
|
||||
&account_fetcher,
|
||||
mango_accounts.iter(),
|
||||
&liq_config,
|
||||
&rebalance_config,
|
||||
)?;
|
||||
},
|
||||
|
||||
_ = rebalance_interval.tick() => {
|
||||
if one_snapshot_done {
|
||||
if let Err(err) = rebalance::zero_all_non_quote(&mango_client, &account_fetcher, &cli.liqor_mango_account, &rebalance_config) {
|
||||
log::error!("failed to rebalance liqor: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn liquidate<'a>(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
accounts: impl Iterator<Item = &'a Pubkey>,
|
||||
config: &liquidate::Config,
|
||||
rebalance_config: &rebalance::Config,
|
||||
) -> anyhow::Result<()> {
|
||||
if !liquidate::maybe_liquidate_one(&mango_client, &account_fetcher, accounts, &config) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let liqor = &mango_client.mango_account_address;
|
||||
if let Err(err) =
|
||||
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, &rebalance_config)
|
||||
{
|
||||
log::error!("failed to rebalance liqor: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_chain_data_metrics(chain: Arc<RwLock<chain_data::ChainData>>, metrics: &metrics::Metrics) {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
|
||||
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
use crate::{account_shared_data::KeyedAccountSharedData, AnyhowWrap};
|
||||
|
||||
use client::{chain_data, AccountFetcher, MangoClient, TokenContext};
|
||||
use mango_v4::state::{oracle_price, Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX};
|
||||
|
||||
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
pub struct Config {
|
||||
pub slippage: f64,
|
||||
pub refresh_timeout: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TokenState {
|
||||
price: I80F48,
|
||||
native_position: I80F48,
|
||||
}
|
||||
|
||||
impl TokenState {
|
||||
fn new_position(
|
||||
token: &TokenContext,
|
||||
position: &TokenPosition,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<Self> {
|
||||
let bank = Self::bank(token, account_fetcher)?;
|
||||
Ok(Self {
|
||||
price: Self::fetch_price(token, &bank, account_fetcher)?,
|
||||
native_position: position.native(&bank),
|
||||
})
|
||||
}
|
||||
|
||||
fn bank(
|
||||
token: &TokenContext,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<Bank> {
|
||||
account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())
|
||||
}
|
||||
|
||||
fn fetch_price(
|
||||
token: &TokenContext,
|
||||
bank: &Bank,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<I80F48> {
|
||||
let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?;
|
||||
oracle_price(
|
||||
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||
bank.oracle_config.conf_filter,
|
||||
bank.mint_decimals,
|
||||
)
|
||||
.map_err_anyhow()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn zero_all_non_quote(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
mango_account_address: &Pubkey,
|
||||
config: &Config,
|
||||
) -> anyhow::Result<()> {
|
||||
log::trace!("checking for rebalance: {}", mango_account_address);
|
||||
|
||||
// TODO: configurable?
|
||||
let quote_token = mango_client.context.token(QUOTE_TOKEN_INDEX);
|
||||
|
||||
let account = account_fetcher.fetch_mango_account(mango_account_address)?;
|
||||
|
||||
let tokens = account
|
||||
.token_iter_active()
|
||||
.map(|token_position| {
|
||||
let token = mango_client.context.token(token_position.token_index);
|
||||
Ok((
|
||||
token.token_index,
|
||||
TokenState::new_position(token, token_position, account_fetcher)?,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<HashMap<TokenIndex, TokenState>>>()?;
|
||||
log::trace!("account tokens: {:?}", tokens);
|
||||
|
||||
// Function to refresh the mango account after the txsig confirmed. Returns false on timeout.
|
||||
let refresh_mango_account =
|
||||
|account_fetcher: &chain_data::AccountFetcher, txsig| -> anyhow::Result<bool> {
|
||||
let max_slot = account_fetcher.transaction_max_slot(&[txsig])?;
|
||||
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
|
||||
&[*mango_account_address],
|
||||
max_slot,
|
||||
config.refresh_timeout,
|
||||
) {
|
||||
// If we don't get fresh data, maybe the tx landed on a fork?
|
||||
// Rebalance is technically still ok.
|
||||
log::info!("could not refresh account data: {}", e);
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(true)
|
||||
};
|
||||
|
||||
for (token_index, token_state) in tokens {
|
||||
let token = mango_client.context.token(token_index);
|
||||
if token_index == quote_token.token_index {
|
||||
continue;
|
||||
}
|
||||
let token_mint = token.mint_info.mint;
|
||||
let quote_mint = quote_token.mint_info.mint;
|
||||
|
||||
// It's not always possible to bring the native balance to 0 through swaps:
|
||||
// Consider a price <1. You need to sell a bunch of tokens to get 1 USDC native and
|
||||
// similarly will get multiple tokens when buying.
|
||||
// Imagine SOL at 0.04 USDC-native per SOL-native: Any amounts below 25 SOL-native
|
||||
// would not be worth a single USDC-native.
|
||||
//
|
||||
// To avoid errors, we consider all amounts below 2 * (1/oracle) dust and don't try
|
||||
// to sell them. Instead they will be withdrawn at the end.
|
||||
// Purchases will aim to purchase slightly more than is needed, such that we can
|
||||
// again withdraw the dust at the end.
|
||||
let dust_threshold = I80F48::from(2) / token_state.price;
|
||||
|
||||
let mut amount = token_state.native_position;
|
||||
|
||||
if amount > dust_threshold {
|
||||
// Sell
|
||||
let txsig = mango_client.jupiter_swap(
|
||||
token_mint,
|
||||
quote_mint,
|
||||
amount.to_num::<u64>(),
|
||||
config.slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)?;
|
||||
log::info!(
|
||||
"sold {} {} for {} in tx {}",
|
||||
token.native_to_ui(amount),
|
||||
token.name,
|
||||
quote_token.name,
|
||||
txsig
|
||||
);
|
||||
if !refresh_mango_account(account_fetcher, txsig)? {
|
||||
return Ok(());
|
||||
}
|
||||
let bank = TokenState::bank(token, account_fetcher)?;
|
||||
amount = mango_client
|
||||
.mango_account()?
|
||||
.token_get(token_index)
|
||||
.map(|(position, _)| position.native(&bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
} else if token_state.native_position < 0 {
|
||||
// Buy
|
||||
let buy_amount = (-token_state.native_position).ceil()
|
||||
+ (dust_threshold - I80F48::ONE).max(I80F48::ZERO);
|
||||
let txsig = mango_client.jupiter_swap(
|
||||
quote_mint,
|
||||
token_mint,
|
||||
buy_amount.to_num::<u64>(),
|
||||
config.slippage,
|
||||
client::JupiterSwapMode::ExactOut,
|
||||
)?;
|
||||
log::info!(
|
||||
"bought {} {} for {} in tx {}",
|
||||
token.native_to_ui(buy_amount),
|
||||
token.name,
|
||||
quote_token.name,
|
||||
txsig
|
||||
);
|
||||
if !refresh_mango_account(account_fetcher, txsig)? {
|
||||
return Ok(());
|
||||
}
|
||||
let bank = TokenState::bank(token, account_fetcher)?;
|
||||
amount = mango_client
|
||||
.mango_account()?
|
||||
.token_get(token_index)
|
||||
.map(|(position, _)| position.native(&bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
}
|
||||
|
||||
// Any remainder that could not be sold just gets withdrawn to ensure the
|
||||
// TokenPosition is freed up
|
||||
if amount > 0 && amount <= dust_threshold {
|
||||
// TODO: fix to false once program updated to fix allow_borrow bug
|
||||
let allow_borrow = true;
|
||||
let txsig =
|
||||
mango_client.token_withdraw(token_mint, amount.to_num::<u64>(), allow_borrow)?;
|
||||
log::info!(
|
||||
"withdrew {} {} to liqor wallet in {}",
|
||||
token.native_to_ui(amount),
|
||||
token.name,
|
||||
txsig
|
||||
);
|
||||
if !refresh_mango_account(account_fetcher, txsig)? {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"unexpected {} position after rebalance swap: {} native",
|
||||
token.name,
|
||||
amount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -86,7 +86,7 @@ pub struct Config {
|
|||
|
||||
async fn feed_snapshots(
|
||||
config: &Config,
|
||||
mango_pyth_oracles: Vec<Pubkey>,
|
||||
mango_oracles: Vec<Pubkey>,
|
||||
sender: &async_channel::Sender<AccountSnapshot>,
|
||||
) -> anyhow::Result<()> {
|
||||
let rpc_client = http::connect_with_options::<AccountsDataClient>(&config.rpc_http_url, true)
|
||||
|
@ -128,7 +128,7 @@ async fn feed_snapshots(
|
|||
let results: Vec<(
|
||||
Vec<Pubkey>,
|
||||
Result<Response<Vec<Option<UiAccount>>>, jsonrpc_core_client::RpcError>,
|
||||
)> = stream::iter(mango_pyth_oracles)
|
||||
)> = stream::iter(mango_oracles)
|
||||
.chunks(config.get_multiple_accounts_count)
|
||||
.map(|keys| {
|
||||
let rpc_client = &rpc_client;
|
||||
|
@ -207,7 +207,7 @@ async fn feed_snapshots(
|
|||
|
||||
pub fn start(
|
||||
config: Config,
|
||||
mango_pyth_oracles: Vec<Pubkey>,
|
||||
mango_oracles: Vec<Pubkey>,
|
||||
sender: async_channel::Sender<AccountSnapshot>,
|
||||
) {
|
||||
let mut poll_wait_first_snapshot = time::interval(time::Duration::from_secs(2));
|
||||
|
@ -239,7 +239,7 @@ pub fn start(
|
|||
|
||||
loop {
|
||||
interval_between_snapshots.tick().await;
|
||||
if let Err(err) = feed_snapshots(&config, mango_pyth_oracles.clone(), &sender).await {
|
||||
if let Err(err) = feed_snapshots(&config, mango_oracles.clone(), &sender).await {
|
||||
warn!("snapshot error: {:?}", err);
|
||||
} else {
|
||||
info!("snapshot success");
|
||||
|
|
|
@ -57,7 +57,7 @@ pub struct Config {
|
|||
|
||||
async fn feed_data(
|
||||
config: &Config,
|
||||
mango_pyth_oracles: Vec<Pubkey>,
|
||||
mango_oracles: Vec<Pubkey>,
|
||||
sender: async_channel::Sender<Message>,
|
||||
) -> anyhow::Result<()> {
|
||||
let connect = ws::try_connect::<RpcSolPubSubClient>(&config.rpc_ws_url).map_err_anyhow()?;
|
||||
|
@ -99,10 +99,9 @@ async fn feed_data(
|
|||
Some(all_accounts_config.clone()),
|
||||
)
|
||||
.map_err_anyhow()?;
|
||||
// TODO: mango_pyth_oracles should not contain stub mango_pyth_oracles, since they already sub'ed with mango_sub
|
||||
let mut mango_pyth_oracles_sub_map = StreamMap::new();
|
||||
for oracle in mango_pyth_oracles.into_iter() {
|
||||
mango_pyth_oracles_sub_map.insert(
|
||||
let mut mango_oracles_sub_map = StreamMap::new();
|
||||
for oracle in mango_oracles.into_iter() {
|
||||
mango_oracles_sub_map.insert(
|
||||
oracle,
|
||||
client
|
||||
.account_subscribe(
|
||||
|
@ -136,13 +135,13 @@ async fn feed_data(
|
|||
return Ok(());
|
||||
}
|
||||
},
|
||||
message = mango_pyth_oracles_sub_map.next() => {
|
||||
message = mango_oracles_sub_map.next() => {
|
||||
if let Some(data) = message {
|
||||
let response = data.1.map_err_anyhow()?;
|
||||
let response = solana_client::rpc_response::Response{ context: RpcResponseContext{ slot: response.context.slot, api_version: None }, value: RpcKeyedAccount{ pubkey: data.0.to_string(), account: response.value} } ;
|
||||
sender.send(Message::Account(AccountUpdate::from_rpc(response)?)).await.expect("sending must succeed");
|
||||
} else {
|
||||
warn!("pyth stream closed");
|
||||
warn!("oracle stream closed");
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
|
@ -171,16 +170,12 @@ async fn feed_data(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn start(
|
||||
config: Config,
|
||||
mango_pyth_oracles: Vec<Pubkey>,
|
||||
sender: async_channel::Sender<Message>,
|
||||
) {
|
||||
pub fn start(config: Config, mango_oracles: Vec<Pubkey>, sender: async_channel::Sender<Message>) {
|
||||
tokio::spawn(async move {
|
||||
// if the websocket disconnects, we get no data in a while etc, reconnect and try again
|
||||
loop {
|
||||
info!("connecting to solana websocket streams");
|
||||
let out = feed_data(&config, mango_pyth_oracles.clone(), sender.clone());
|
||||
let out = feed_data(&config, mango_oracles.clone(), sender.clone());
|
||||
let _ = out.await;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -85,8 +85,8 @@ pub fn liq_token_with_token(
|
|||
// The main complication here is that we can't keep the liqee_asset_position and liqee_liab_position
|
||||
// borrows alive at the same time. Possibly adding get_mut_pair() would be helpful.
|
||||
let (liqee_asset_position, liqee_asset_raw_index) = liqee.token_get(asset_token_index)?;
|
||||
let liqee_assets_native = liqee_asset_position.native(asset_bank);
|
||||
require!(liqee_assets_native.is_positive(), MangoError::SomeError);
|
||||
let liqee_asset_native = liqee_asset_position.native(asset_bank);
|
||||
require!(liqee_asset_native.is_positive(), MangoError::SomeError);
|
||||
|
||||
let (liqee_liab_position, liqee_liab_raw_index) = liqee.token_get(liab_token_index)?;
|
||||
let liqee_liab_native = liqee_liab_position.native(liab_bank);
|
||||
|
@ -115,7 +115,7 @@ pub fn liq_token_with_token(
|
|||
/ (liab_price * init_liab_weight - init_asset_weight * liab_price_adjusted));
|
||||
|
||||
// How much liab can we get at most for the asset balance?
|
||||
let liab_possible = cm!(liqee_assets_native * asset_price / liab_price_adjusted);
|
||||
let liab_possible = cm!(liqee_asset_native * asset_price / liab_price_adjusted);
|
||||
|
||||
// The amount of liab native tokens we will transfer
|
||||
let liab_transfer = min(
|
||||
|
@ -135,6 +135,7 @@ pub fn liq_token_with_token(
|
|||
liqor.token_get_mut_or_create(liab_token_index)?;
|
||||
let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?;
|
||||
let liqor_liab_position_indexed = liqor_liab_position.indexed_position;
|
||||
let liqee_liab_native_after = liqee_liab_position.native(&liab_bank);
|
||||
|
||||
let (liqor_asset_position, liqor_asset_raw_index, _) =
|
||||
liqor.token_get_mut_or_create(asset_token_index)?;
|
||||
|
@ -145,10 +146,17 @@ pub fn liq_token_with_token(
|
|||
let liqee_asset_active =
|
||||
asset_bank.withdraw_without_fee(liqee_asset_position, asset_transfer)?;
|
||||
let liqee_asset_position_indexed = liqee_asset_position.indexed_position;
|
||||
let liqee_assets_native_after = liqee_asset_position.native(&asset_bank);
|
||||
|
||||
// Update the health cache
|
||||
liqee_health_cache.adjust_token_balance(liab_token_index, liab_transfer)?;
|
||||
liqee_health_cache.adjust_token_balance(asset_token_index, -asset_transfer)?;
|
||||
liqee_health_cache.adjust_token_balance(
|
||||
liab_token_index,
|
||||
cm!(liqee_liab_native_after - liqee_liab_native),
|
||||
)?;
|
||||
liqee_health_cache.adjust_token_balance(
|
||||
asset_token_index,
|
||||
cm!(liqee_assets_native_after - liqee_asset_native),
|
||||
)?;
|
||||
|
||||
msg!(
|
||||
"liquidated {} liab for {} asset",
|
||||
|
|
|
@ -505,5 +505,73 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
assert!(!liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: bankruptcy when collateral is dusted
|
||||
//
|
||||
|
||||
// Setup: make collateral really valueable, remove nearly all of it
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: collateral_token1.mint.pubkey,
|
||||
payer,
|
||||
price: "100000.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: (account_position(solana, account, collateral_token1.bank).await) as u64 - 1,
|
||||
allow_borrow: false,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[2],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Setup: reduce collateral value to trigger liquidatability
|
||||
// We have -93 borrows, so -93*2*1.4 = -260.4 health from that
|
||||
// And 1-2 collateral, so max 2*0.6*X health; say X=150 for max 180 health
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: collateral_token1.mint.pubkey,
|
||||
payer,
|
||||
price: "150.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenWithTokenInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
asset_token_index: collateral_token1.index,
|
||||
liab_token_index: borrow_token1.index,
|
||||
max_liab_transfer: I80F48::from_num(10001.0),
|
||||
asset_bank_index: 0,
|
||||
liab_bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Liqee's remaining collateral got dusted, only borrows remain: bankrupt
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert_eq!(liqee.token_iter_active().count(), 1);
|
||||
assert!(liqee.is_bankrupt());
|
||||
assert!(liqee.being_liquidated());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -128,10 +128,7 @@ export class MangoClient {
|
|||
return group;
|
||||
}
|
||||
|
||||
public async getGroupForCreator(
|
||||
creatorPk: PublicKey,
|
||||
groupNum?: number,
|
||||
): Promise<Group> {
|
||||
public async getGroupsForCreator(creatorPk: PublicKey): Promise<Group[]> {
|
||||
const filters: MemcmpFilter[] = [
|
||||
{
|
||||
memcmp: {
|
||||
|
@ -141,20 +138,25 @@ export class MangoClient {
|
|||
},
|
||||
];
|
||||
|
||||
if (groupNum !== undefined) {
|
||||
const bbuf = Buffer.alloc(4);
|
||||
bbuf.writeUInt32LE(groupNum);
|
||||
filters.push({
|
||||
memcmp: {
|
||||
bytes: bs58.encode(bbuf),
|
||||
offset: 40,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const groups = (await this.program.account.group.all(filters)).map(
|
||||
(tuple) => Group.from(tuple.publicKey, tuple.account),
|
||||
return (await this.program.account.group.all(filters)).map((tuple) =>
|
||||
Group.from(tuple.publicKey, tuple.account),
|
||||
);
|
||||
}
|
||||
|
||||
public async getGroupForCreator(
|
||||
creatorPk: PublicKey,
|
||||
groupNum?: number,
|
||||
): Promise<Group> {
|
||||
const groups = (await this.getGroupsForCreator(creatorPk)).filter(
|
||||
(group) => {
|
||||
if (groupNum !== undefined) {
|
||||
return group.groupNum == groupNum;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await groups[0].reloadAll(this);
|
||||
return groups[0];
|
||||
}
|
||||
|
@ -472,12 +474,35 @@ export class MangoClient {
|
|||
accountNumber?: number,
|
||||
name?: string,
|
||||
): Promise<MangoAccount> {
|
||||
let mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk);
|
||||
if (mangoAccounts.length === 0) {
|
||||
await this.createMangoAccount(group, accountNumber, name);
|
||||
mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk);
|
||||
// TODO: this function discards accountSize and name when the account exists already!
|
||||
// TODO: this function always creates accounts for this.program.owner, and not
|
||||
// ownerPk! It needs to get passed a keypair, and we need to add
|
||||
// createMangoAccountForOwner
|
||||
if (accountNumber === undefined) {
|
||||
// Get any MangoAccount
|
||||
// TODO: should probably sort by accountNum for deterministic output!
|
||||
let mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk);
|
||||
if (mangoAccounts.length === 0) {
|
||||
await this.createMangoAccount(group, accountNumber, name);
|
||||
mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk);
|
||||
}
|
||||
return mangoAccounts[0];
|
||||
} else {
|
||||
let account = await this.getMangoAccountForOwner(
|
||||
group,
|
||||
ownerPk,
|
||||
accountNumber,
|
||||
);
|
||||
if (account === undefined) {
|
||||
await this.createMangoAccount(group, accountNumber, name);
|
||||
account = await this.getMangoAccountForOwner(
|
||||
group,
|
||||
ownerPk,
|
||||
accountNumber,
|
||||
);
|
||||
}
|
||||
return account;
|
||||
}
|
||||
return mangoAccounts[0];
|
||||
}
|
||||
|
||||
public async createMangoAccount(
|
||||
|
@ -537,6 +562,16 @@ export class MangoClient {
|
|||
);
|
||||
}
|
||||
|
||||
public async getMangoAccountForOwner(
|
||||
group: Group,
|
||||
ownerPk: PublicKey,
|
||||
accountNumber: number,
|
||||
): Promise<MangoAccount> {
|
||||
return (await this.getMangoAccountsForOwner(group, ownerPk)).find(
|
||||
(a) => a.accountNum == accountNumber,
|
||||
);
|
||||
}
|
||||
|
||||
public async getMangoAccountsForOwner(
|
||||
group: Group,
|
||||
ownerPk: PublicKey,
|
||||
|
@ -623,6 +658,22 @@ export class MangoClient {
|
|||
amount: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const bank = group.banksMap.get(tokenName)!;
|
||||
const nativeAmount = toNativeDecimals(amount, bank.mintDecimals).toNumber();
|
||||
return await this.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
tokenName,
|
||||
nativeAmount,
|
||||
);
|
||||
}
|
||||
|
||||
public async tokenDepositNative(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
tokenName: string,
|
||||
nativeAmount: number,
|
||||
) {
|
||||
const bank = group.banksMap.get(tokenName)!;
|
||||
|
||||
const tokenAccountPk = await getAssociatedTokenAddress(
|
||||
bank.mint,
|
||||
|
@ -635,7 +686,7 @@ export class MangoClient {
|
|||
const additionalSigners: Signer[] = [];
|
||||
if (bank.mint.equals(WRAPPED_SOL_MINT)) {
|
||||
wrappedSolAccount = new Keypair();
|
||||
const lamports = Math.round(amount * LAMPORTS_PER_SOL) + 1e7;
|
||||
const lamports = nativeAmount + 1e7;
|
||||
|
||||
preInstructions = [
|
||||
SystemProgram.createAccount({
|
||||
|
@ -670,7 +721,7 @@ export class MangoClient {
|
|||
);
|
||||
|
||||
return await this.program.methods
|
||||
.tokenDeposit(toNativeDecimals(amount, bank.mintDecimals))
|
||||
.tokenDeposit(new BN(nativeAmount))
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
|
@ -699,45 +750,14 @@ export class MangoClient {
|
|||
allowBorrow: boolean,
|
||||
): Promise<TransactionSignature> {
|
||||
const bank = group.banksMap.get(tokenName)!;
|
||||
|
||||
const tokenAccountPk = await getAssociatedTokenAddress(
|
||||
bank.mint,
|
||||
mangoAccount.owner,
|
||||
const nativeAmount = toNativeDecimals(amount, bank.mintDecimals).toNumber();
|
||||
return await this.tokenWithdrawNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
tokenName,
|
||||
nativeAmount,
|
||||
allowBorrow,
|
||||
);
|
||||
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
group,
|
||||
[mangoAccount],
|
||||
[bank],
|
||||
);
|
||||
|
||||
return await this.program.methods
|
||||
.tokenWithdraw(toNativeDecimals(amount, bank.mintDecimals), allowBorrow)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
bank: bank.publicKey,
|
||||
vault: bank.vault,
|
||||
tokenAccount: tokenAccountPk,
|
||||
owner: mangoAccount.owner,
|
||||
})
|
||||
.remainingAccounts(
|
||||
healthRemainingAccounts.map(
|
||||
(pk) =>
|
||||
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
|
||||
),
|
||||
)
|
||||
.preInstructions([
|
||||
// ensure withdraws don't fail with missing ATAs
|
||||
await createAssociatedTokenAccountIdempotentInstruction(
|
||||
mangoAccount.owner,
|
||||
mangoAccount.owner,
|
||||
bank.mint,
|
||||
),
|
||||
])
|
||||
.rpc({ skipPreflight: true });
|
||||
}
|
||||
|
||||
public async tokenWithdrawNative(
|
||||
|
@ -778,6 +798,14 @@ export class MangoClient {
|
|||
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
|
||||
),
|
||||
)
|
||||
.preInstructions([
|
||||
// ensure withdraws don't fail with missing ATAs
|
||||
await createAssociatedTokenAccountIdempotentInstruction(
|
||||
mangoAccount.owner,
|
||||
mangoAccount.owner,
|
||||
bank.mint,
|
||||
),
|
||||
])
|
||||
.rpc({ skipPreflight: true });
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import { BN, BorshCoder } from '@project-serum/anchor';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
import { IDL } from '../mango_v4';
|
||||
|
||||
async function main() {
|
||||
const coder = new BorshCoder(IDL);
|
||||
|
||||
const event = coder.events.decode(process.argv[2]);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
event,
|
||||
function (key, value) {
|
||||
const orig_value = this[key]; // value is already processed
|
||||
if (orig_value instanceof BN) {
|
||||
return orig_value.toString();
|
||||
}
|
||||
return value;
|
||||
},
|
||||
' ',
|
||||
),
|
||||
);
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -8,11 +8,8 @@ import { MANGO_V4_ID } from '../constants';
|
|||
// example script to close accounts - banks, markets, group etc. which require admin to be the signer
|
||||
//
|
||||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
]);
|
||||
// Use to close only a specific group by number. Use "all" to close all groups.
|
||||
const GROUP_NUM = process.env.GROUP_NUM;
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
|
@ -32,49 +29,58 @@ async function main() {
|
|||
MANGO_V4_ID['mainnet-beta'],
|
||||
);
|
||||
|
||||
const group = await client.getGroupForCreator(admin.publicKey);
|
||||
console.log(`Group ${group.publicKey}`);
|
||||
const groups = await (async () => {
|
||||
if (GROUP_NUM === 'all') {
|
||||
return await client.getGroupsForCreator(admin.publicKey);
|
||||
} else {
|
||||
return [
|
||||
await client.getGroupForCreator(admin.publicKey, Number(GROUP_NUM)),
|
||||
];
|
||||
}
|
||||
})();
|
||||
for (const group of groups) {
|
||||
console.log(`Group ${group.publicKey}`);
|
||||
|
||||
let sig;
|
||||
let sig;
|
||||
|
||||
// close stub oracle
|
||||
const usdcMainnetBetaMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
||||
const usdcMainnetBetaOracle = (
|
||||
await client.getStubOracle(group, usdcMainnetBetaMint)
|
||||
)[0];
|
||||
sig = await client.stubOracleClose(group, usdcMainnetBetaOracle.publicKey);
|
||||
console.log(
|
||||
`Closed USDC stub oracle, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
// close stub oracles
|
||||
const stubOracles = await client.getStubOracle(group);
|
||||
for (const stubOracle of stubOracles) {
|
||||
sig = await client.stubOracleClose(group, stubOracle.publicKey);
|
||||
console.log(
|
||||
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
}
|
||||
|
||||
// close all bank
|
||||
for (const bank of group.banksMap.values()) {
|
||||
sig = await client.tokenDeregister(group, bank.name);
|
||||
console.log(
|
||||
`Removed token ${bank.name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
// close all banks
|
||||
for (const bank of group.banksMap.values()) {
|
||||
sig = await client.tokenDeregister(group, bank.name);
|
||||
console.log(
|
||||
`Removed token ${bank.name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
}
|
||||
|
||||
// deregister all serum markets
|
||||
for (const market of group.serum3MarketsMap.values()) {
|
||||
sig = await client.serum3deregisterMarket(group, market.name);
|
||||
console.log(
|
||||
`Deregistered serum market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
}
|
||||
|
||||
// close all perp markets
|
||||
for (const market of group.perpMarketsMap.values()) {
|
||||
sig = await client.perpCloseMarket(group, market.name);
|
||||
console.log(
|
||||
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
}
|
||||
|
||||
// finally, close the group
|
||||
sig = await client.groupClose(group);
|
||||
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);
|
||||
}
|
||||
|
||||
// deregister all serum markets
|
||||
for (const market of group.serum3MarketsMap.values()) {
|
||||
sig = await client.serum3deregisterMarket(group, market.name);
|
||||
console.log(
|
||||
`Deregistered serum market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
}
|
||||
|
||||
// close all perp markets
|
||||
for (const market of group.perpMarketsMap.values()) {
|
||||
sig = await client.perpCloseMarket(group, market.name);
|
||||
console.log(
|
||||
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
}
|
||||
|
||||
// finally, close the group
|
||||
sig = await client.groupClose(group);
|
||||
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// Script which depoys a new mango group, and registers 3 tokens
|
||||
// with stub oracles
|
||||
//
|
||||
|
||||
// default to group 1, to not conflict with the normal group
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 1);
|
||||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
]);
|
||||
|
||||
const STUB_PRICES = new Map([
|
||||
['USDC', 1.0],
|
||||
['BTC', 20000.0], // btc and usdc both have 6 decimals
|
||||
['SOL', 0.04], // sol has 9 decimals, equivalent to $40 per SOL
|
||||
]);
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL, options);
|
||||
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const adminWallet = new Wallet(admin);
|
||||
console.log(`Admin ${adminWallet.publicKey.toBase58()}`);
|
||||
const adminProvider = new AnchorProvider(connection, adminWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
adminProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
);
|
||||
|
||||
// group
|
||||
console.log(`Creating Group...`);
|
||||
try {
|
||||
const insuranceMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
||||
await client.groupCreate(GROUP_NUM, true, 0, insuranceMint);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(`...registered group ${group.publicKey}`);
|
||||
|
||||
// stub oracles
|
||||
let oracles = new Map();
|
||||
for (let [name, mint] of MAINNET_MINTS) {
|
||||
console.log(`Creating stub oracle for ${name}...`);
|
||||
const mintPk = new PublicKey(mint);
|
||||
try {
|
||||
const price = STUB_PRICES.get(name);
|
||||
await client.stubOracleCreate(group, mintPk, price);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
const oracle = (await client.getStubOracle(group, mintPk))[0];
|
||||
console.log(`...created stub oracle ${oracle.publicKey}`);
|
||||
oracles.set(name, oracle.publicKey);
|
||||
}
|
||||
|
||||
// register token 1
|
||||
console.log(`Registering BTC...`);
|
||||
const btcMainnetMint = new PublicKey(MAINNET_MINTS.get('BTC')!);
|
||||
const btcMainnetOracle = oracles.get('BTC');
|
||||
try {
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
btcMainnetMint,
|
||||
btcMainnetOracle,
|
||||
0.1,
|
||||
1,
|
||||
'BTC',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.7,
|
||||
0.88,
|
||||
1.5,
|
||||
0.0,
|
||||
0.0001,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// register token 0
|
||||
console.log(`Registering USDC...`);
|
||||
const usdcMainnetMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
||||
const usdcMainnetOracle = oracles.get('USDC');
|
||||
try {
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
usdcMainnetMint,
|
||||
usdcMainnetOracle,
|
||||
0.1,
|
||||
0,
|
||||
'USDC',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.9,
|
||||
1.5,
|
||||
0.0,
|
||||
0.0001,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// register token 2
|
||||
console.log(`Registering SOL...`);
|
||||
const solMainnetMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
|
||||
const solMainnetOracle = oracles.get('SOL');
|
||||
try {
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
solMainnetMint,
|
||||
solMainnetOracle,
|
||||
0.1,
|
||||
2, // tokenIndex
|
||||
'SOL',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.9,
|
||||
1.5,
|
||||
0.0,
|
||||
0.0001,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// log tokens/banks
|
||||
for (const bank of await group.banksMap.values()) {
|
||||
console.log(`${bank.toString()}`);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,68 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// This script deposits some tokens, so other liquidation scripts can borrow.
|
||||
//
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 1);
|
||||
const ACCOUNT_NUM = Number(process.env.ACCOUNT_NUM || 0);
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL, options);
|
||||
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(admin);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
// fetch group
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(group.toString());
|
||||
|
||||
// create + fetch account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
const mangoAccount = await client.getOrCreateMangoAccount(
|
||||
group,
|
||||
admin.publicKey,
|
||||
ACCOUNT_NUM,
|
||||
);
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
// deposit
|
||||
try {
|
||||
console.log(`...depositing 10 USDC`);
|
||||
await client.tokenDeposit(group, mangoAccount, 'USDC', 10);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...depositing 0.0004 BTC`);
|
||||
await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0004);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...depositing 0.25 SOL`);
|
||||
await client.tokenDeposit(group, mangoAccount, 'SOL', 0.25);
|
||||
await mangoAccount.reload(client, group);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,110 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// This script creates liquidation candidates
|
||||
//
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 1);
|
||||
|
||||
// native prices
|
||||
const PRICES = {
|
||||
BTC: 20000.0,
|
||||
SOL: 0.04,
|
||||
USDC: 1,
|
||||
};
|
||||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
]);
|
||||
|
||||
const SCENARIOS: [string, string, number, string, number][] = [
|
||||
['LIQTEST, LIQOR', 'USDC', 1000000, 'USDC', 0],
|
||||
['LIQTEST, A: USDC, L: SOL', 'USDC', 1000 * PRICES.SOL, 'SOL', 920],
|
||||
['LIQTEST, A: SOL, L: USDC', 'SOL', 1000, 'USDC', 920 * PRICES.SOL],
|
||||
// TODO: needs the fix on liq+dust to go live
|
||||
//['LIQTEST, A: BTC, L: SOL', 'BTC', 20, 'SOL', 18 * PRICES.BTC / PRICES.SOL],
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL, options);
|
||||
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(admin);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
// fetch group
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(group.toString());
|
||||
|
||||
const accounts = await client.getMangoAccountsForOwner(
|
||||
group,
|
||||
admin.publicKey,
|
||||
);
|
||||
let maxAccountNum = Math.max(...accounts.map((a) => a.accountNum));
|
||||
|
||||
for (const scenario of SCENARIOS) {
|
||||
const [name, assetName, assetAmount, liabName, liabAmount] = scenario;
|
||||
|
||||
// create account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
let mangoAccount = await client.getOrCreateMangoAccount(
|
||||
group,
|
||||
admin.publicKey,
|
||||
maxAccountNum + 1,
|
||||
);
|
||||
maxAccountNum = maxAccountNum + 1;
|
||||
console.log(
|
||||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
assetName,
|
||||
assetAmount,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
if (liabAmount > 0) {
|
||||
// temporarily drop the borrowed token value, so the borrow goes through
|
||||
const oracle = group.banksMap.get(liabName).oracle;
|
||||
try {
|
||||
await client.stubOracleSet(group, oracle, PRICES[liabName] / 2);
|
||||
|
||||
await client.tokenWithdrawNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
liabName,
|
||||
liabAmount,
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
// restore the oracle
|
||||
await client.stubOracleSet(group, oracle, PRICES[liabName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,102 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// This script tries to withdraw all positive balances for all accounts
|
||||
// by MANGO_MAINNET_PAYER_KEYPAIR in the group.
|
||||
//
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 1);
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL, options);
|
||||
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(admin);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(group.toString());
|
||||
|
||||
let accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
|
||||
for (let account of accounts) {
|
||||
console.log(`settling borrows on account: ${account}`);
|
||||
|
||||
// first, settle all borrows
|
||||
for (let token of account.tokensActive()) {
|
||||
const bank = group.findBank(token.tokenIndex);
|
||||
const amount = token.native(bank).toNumber();
|
||||
if (amount < 0) {
|
||||
try {
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
account,
|
||||
bank.name,
|
||||
Math.ceil(-amount),
|
||||
);
|
||||
await account.reload(client, group);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`failed to deposit ${bank.name} into ${account.publicKey}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
|
||||
for (let account of accounts) {
|
||||
console.log(`withdrawing deposits of account: ${account}`);
|
||||
|
||||
// withdraw all funds
|
||||
for (let token of account.tokensActive()) {
|
||||
const bank = group.findBank(token.tokenIndex);
|
||||
const amount = token.native(bank).toNumber();
|
||||
if (amount > 0) {
|
||||
try {
|
||||
const allowBorrow = true; // TODO: set this to false once the withdraw amount ___<___ nativePosition bug is fixed
|
||||
await client.tokenWithdrawNative(
|
||||
group,
|
||||
account,
|
||||
bank.name,
|
||||
amount,
|
||||
allowBorrow,
|
||||
);
|
||||
await account.reload(client, group);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`failed to withdraw ${bank.name} from ${account.publicKey}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// close account
|
||||
try {
|
||||
console.log(`closing account: ${account}`);
|
||||
await client.closeMangoAccount(group, account);
|
||||
} catch (error) {
|
||||
console.log(`failed to close ${account.publicKey}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
Loading…
Reference in New Issue