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-account-decoder",
|
||||||
"solana-client",
|
"solana-client",
|
||||||
"solana-sdk",
|
"solana-sdk",
|
||||||
|
"spl-associated-token-account",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
|
@ -176,7 +176,13 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
let input_mint = client::pubkey_from_cli(&cmd.input_mint);
|
let input_mint = client::pubkey_from_cli(&cmd.input_mint);
|
||||||
let output_mint = client::pubkey_from_cli(&cmd.output_mint);
|
let output_mint = client::pubkey_from_cli(&cmd.output_mint);
|
||||||
let client = MangoClient::new_for_existing_account(client, account, owner)?;
|
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);
|
println!("{}", txsig);
|
||||||
}
|
}
|
||||||
Command::GroupAddress { creator, num } => {
|
Command::GroupAddress { creator, num } => {
|
||||||
|
|
|
@ -21,6 +21,7 @@ shellexpand = "2.1.0"
|
||||||
solana-account-decoder = "~1.10.29"
|
solana-account-decoder = "~1.10.29"
|
||||||
solana-client = "~1.10.29"
|
solana-client = "~1.10.29"
|
||||||
solana-sdk = "~1.10.29"
|
solana-sdk = "~1.10.29"
|
||||||
|
spl-associated-token-account = "1.0.3"
|
||||||
thiserror = "1.0.31"
|
thiserror = "1.0.31"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
reqwest = "0.11.11"
|
reqwest = "0.11.11"
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::chain_data::*;
|
use crate::chain_data::*;
|
||||||
|
|
||||||
|
@ -11,7 +13,9 @@ use anyhow::Context;
|
||||||
|
|
||||||
use solana_client::rpc_client::RpcClient;
|
use solana_client::rpc_client::RpcClient;
|
||||||
use solana_sdk::account::{AccountSharedData, ReadableAccount};
|
use solana_sdk::account::{AccountSharedData, ReadableAccount};
|
||||||
|
use solana_sdk::clock::Slot;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
use solana_sdk::signature::Signature;
|
||||||
|
|
||||||
pub struct AccountFetcher {
|
pub struct AccountFetcher {
|
||||||
pub chain_data: Arc<RwLock<ChainData>>,
|
pub chain_data: Arc<RwLock<ChainData>>,
|
||||||
|
@ -66,11 +70,12 @@ impl AccountFetcher {
|
||||||
.clone())
|
.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
|
let response = self
|
||||||
.rpc
|
.rpc
|
||||||
.get_account_with_commitment(&address, self.rpc.commitment())
|
.get_account_with_commitment(&address, self.rpc.commitment())
|
||||||
.with_context(|| format!("refresh account {} via rpc", address))?;
|
.with_context(|| format!("refresh account {} via rpc", address))?;
|
||||||
|
let slot = response.context.slot;
|
||||||
let account = response
|
let account = response
|
||||||
.value
|
.value
|
||||||
.ok_or(anchor_client::ClientError::AccountNotFound)
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -395,6 +395,57 @@ impl MangoClient {
|
||||||
.map_err(prettify_client_error)
|
.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(
|
pub fn get_oracle_price(
|
||||||
&self,
|
&self,
|
||||||
token_name: &str,
|
token_name: &str,
|
||||||
|
@ -837,33 +888,50 @@ impl MangoClient {
|
||||||
&self,
|
&self,
|
||||||
input_mint: Pubkey,
|
input_mint: Pubkey,
|
||||||
output_mint: Pubkey,
|
output_mint: Pubkey,
|
||||||
source_amount: u64,
|
amount: u64,
|
||||||
slippage: f64,
|
slippage: f64,
|
||||||
|
swap_mode: JupiterSwapMode,
|
||||||
) -> anyhow::Result<Signature> {
|
) -> 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 fn jupiter_route(
|
||||||
pub async fn jupiter_swap_async(
|
|
||||||
&self,
|
&self,
|
||||||
input_mint: Pubkey,
|
input_mint: Pubkey,
|
||||||
output_mint: Pubkey,
|
output_mint: Pubkey,
|
||||||
source_amount: u64,
|
amount: u64,
|
||||||
slippage: f64,
|
slippage: f64,
|
||||||
) -> anyhow::Result<Signature> {
|
swap_mode: JupiterSwapMode,
|
||||||
let source_token = self.context.token_by_mint(&input_mint)?;
|
) -> anyhow::Result<jupiter::QueryRoute> {
|
||||||
let target_token = self.context.token_by_mint(&output_mint)?;
|
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
|
let quote = self
|
||||||
.http_client
|
.http_client
|
||||||
.get("https://quote-api.jup.ag/v1/quote")
|
.get("https://quote-api.jup.ag/v1/quote")
|
||||||
.query(&[
|
.query(&[
|
||||||
("inputMint", input_mint.to_string()),
|
("inputMint", input_mint.to_string()),
|
||||||
("outputMint", output_mint.to_string()),
|
("outputMint", output_mint.to_string()),
|
||||||
("amount", format!("{}", source_amount)),
|
("amount", format!("{}", amount)),
|
||||||
("onlyDirectRoutes", "true".into()),
|
("onlyDirectRoutes", "true".into()),
|
||||||
("filterTopNResult", "10".into()),
|
("filterTopNResult", "10".into()),
|
||||||
("slippage", format!("{}", slippage)),
|
("slippage", format!("{}", slippage)),
|
||||||
|
(
|
||||||
|
"swapMode",
|
||||||
|
match swap_mode {
|
||||||
|
JupiterSwapMode::ExactIn => "ExactIn",
|
||||||
|
JupiterSwapMode::ExactOut => "ExactOut",
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
.await
|
.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
|
let swap = self
|
||||||
.http_client
|
.http_client
|
||||||
.post("https://quote-api.jup.ag/v1/swap")
|
.post("https://quote-api.jup.ag/v1/swap")
|
||||||
|
@ -919,19 +1004,12 @@ impl MangoClient {
|
||||||
.context("base64 decoding jupiter transaction")?,
|
.context("base64 decoding jupiter transaction")?,
|
||||||
)
|
)
|
||||||
.context("parsing 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)
|
let jup_ixs = deserialize_instructions(&jup_tx.message)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// TODO: possibly creating associated token accounts if they don't exist yet is good?!
|
.filter(|ix| !is_setup_ix(ix.program_id))
|
||||||
// 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()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let bank_ams = [
|
let bank_ams = [
|
||||||
|
@ -962,7 +1040,14 @@ impl MangoClient {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.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!
|
// This relies on the fact that health account banks will be identical to the first_bank above!
|
||||||
let health_ams = self
|
let health_ams = self
|
||||||
|
@ -973,7 +1058,19 @@ impl MangoClient {
|
||||||
.context("building health accounts")?;
|
.context("building health accounts")?;
|
||||||
|
|
||||||
let program = self.program();
|
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(),
|
program_id: mango_v4::id(),
|
||||||
accounts: {
|
accounts: {
|
||||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||||
|
@ -994,7 +1091,7 @@ impl MangoClient {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
for ix in jup_ixs {
|
for ix in jup_ixs {
|
||||||
builder = builder.instruction(ix);
|
builder = builder.instruction(ix.clone());
|
||||||
}
|
}
|
||||||
builder = builder.instruction(Instruction {
|
builder = builder.instruction(Instruction {
|
||||||
program_id: mango_v4::id(),
|
program_id: mango_v4::id(),
|
||||||
|
@ -1094,6 +1191,12 @@ pub fn prettify_client_error(err: anchor_client::ClientError) -> anyhow::Error {
|
||||||
err.into()
|
err.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum JupiterSwapMode {
|
||||||
|
ExactIn,
|
||||||
|
ExactOut,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn keypair_from_cli(keypair: &str) -> Keypair {
|
pub fn keypair_from_cli(keypair: &str) -> Keypair {
|
||||||
let maybe_keypair = keypair::read_keypair(&mut keypair.as_bytes());
|
let maybe_keypair = keypair::read_keypair(&mut keypair.as_bytes());
|
||||||
match maybe_keypair {
|
match maybe_keypair {
|
||||||
|
@ -1128,3 +1231,17 @@ fn to_writable_account_meta(pubkey: Pubkey) -> AccountMeta {
|
||||||
is_signer: false,
|
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 out_amount: u64,
|
||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
pub other_amount_threshold: u64,
|
pub other_amount_threshold: u64,
|
||||||
pub out_amount_with_slippage: u64,
|
pub out_amount_with_slippage: Option<u64>,
|
||||||
pub swap_mode: String,
|
pub swap_mode: String,
|
||||||
pub price_impact_pct: f64,
|
pub price_impact_pct: f64,
|
||||||
pub market_infos: Vec<QueryMarketInfo>,
|
pub market_infos: Vec<QueryMarketInfo>,
|
||||||
|
@ -33,7 +33,7 @@ pub struct QueryMarketInfo {
|
||||||
pub lp_fee: QueryFee,
|
pub lp_fee: QueryFee,
|
||||||
pub platform_fee: QueryFee,
|
pub platform_fee: QueryFee,
|
||||||
pub not_enough_liquidity: bool,
|
pub not_enough_liquidity: bool,
|
||||||
pub price_impact_pct: f64,
|
pub price_impact_pct: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
@ -41,7 +41,7 @@ pub struct QueryMarketInfo {
|
||||||
pub struct QueryFee {
|
pub struct QueryFee {
|
||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
pub mint: String,
|
pub mint: String,
|
||||||
pub pct: f64,
|
pub pct: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
@ -49,6 +49,7 @@ pub struct QueryFee {
|
||||||
pub struct SwapRequest {
|
pub struct SwapRequest {
|
||||||
pub route: QueryRoute,
|
pub route: QueryRoute,
|
||||||
pub user_public_key: String,
|
pub user_public_key: String,
|
||||||
|
#[serde(rename = "wrapUnwrapSOL")]
|
||||||
pub wrap_unwrap_sol: bool,
|
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)]
|
// #[allow(dead_code)]
|
||||||
// pub fn retry<T>(request: impl Fn() -> Result<T, anchor_client::ClientError>) -> anyhow::Result<T> {
|
// 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()
|
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 crate::account_shared_data::KeyedAccountSharedData;
|
||||||
|
|
||||||
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
|
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
|
||||||
use mango_v4::state::{
|
use mango_v4::state::{
|
||||||
new_health_cache, oracle_price, Bank, FixedOrderAccountRetriever, HealthCache, HealthType,
|
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};
|
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_(
|
pub fn new_health_cache_(
|
||||||
context: &MangoGroupContext,
|
context: &MangoGroupContext,
|
||||||
account_fetcher: &chain_data::AccountFetcher,
|
account_fetcher: &chain_data::AccountFetcher,
|
||||||
|
@ -36,14 +43,68 @@ pub fn new_health_cache_(
|
||||||
new_health_cache(&account.borrow(), &retriever).context("make 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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn process_account(
|
pub fn maybe_liquidate_account(
|
||||||
mango_client: &MangoClient,
|
mango_client: &MangoClient,
|
||||||
account_fetcher: &chain_data::AccountFetcher,
|
account_fetcher: &chain_data::AccountFetcher,
|
||||||
pubkey: &Pubkey,
|
pubkey: &Pubkey,
|
||||||
) -> anyhow::Result<()> {
|
config: &Config,
|
||||||
// TODO: configurable
|
) -> anyhow::Result<bool> {
|
||||||
let min_health_ratio = I80F48::from_num(50.0f64);
|
let min_health_ratio = I80F48::from_num(config.min_health_ratio);
|
||||||
let quote_token_index = 0;
|
let quote_token_index = 0;
|
||||||
|
|
||||||
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
||||||
|
@ -52,7 +113,7 @@ pub fn process_account(
|
||||||
.health(HealthType::Maint);
|
.health(HealthType::Maint);
|
||||||
|
|
||||||
if maint_health >= 0 && !account.is_bankrupt() {
|
if maint_health >= 0 && !account.is_bankrupt() {
|
||||||
return Ok(());
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
log::trace!(
|
log::trace!(
|
||||||
|
@ -85,11 +146,11 @@ pub fn process_account(
|
||||||
)?;
|
)?;
|
||||||
Ok((
|
Ok((
|
||||||
token_position.token_index,
|
token_position.token_index,
|
||||||
bank,
|
price,
|
||||||
token_position.native(&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));
|
tokens.sort_by(|a, b| a.2.cmp(&b.2));
|
||||||
|
|
||||||
let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
|
let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
|
||||||
|
@ -111,31 +172,69 @@ pub fn process_account(
|
||||||
};
|
};
|
||||||
|
|
||||||
// try liquidating
|
// try liquidating
|
||||||
if account.is_bankrupt() {
|
let txsig = if account.is_bankrupt() {
|
||||||
if tokens.is_empty() {
|
if tokens.is_empty() {
|
||||||
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey);
|
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
|
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")?;
|
.context("sending liq_token_bankruptcy")?;
|
||||||
log::info!(
|
log::info!(
|
||||||
"Liquidated bankruptcy for {}..., maint_health was {}, tx sig {:?}",
|
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
|
||||||
&pubkey.to_string()[..3],
|
pubkey,
|
||||||
maint_health,
|
maint_health,
|
||||||
sig
|
sig
|
||||||
);
|
);
|
||||||
|
sig
|
||||||
} else if maint_health.is_negative() {
|
} else if maint_health.is_negative() {
|
||||||
if tokens.len() < 2 {
|
let asset_token_index = tokens
|
||||||
anyhow::bail!("mango account {}, has less than 2 active tokens", pubkey);
|
.iter()
|
||||||
}
|
.rev()
|
||||||
let (asset_token_index, _asset_bank, _asset_price) = tokens.last().unwrap();
|
.find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| {
|
||||||
let (liab_token_index, _liab_bank, _liab_price) = tokens.first().unwrap();
|
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")?;
|
.context("getting max_liab_transfer")?;
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -146,29 +245,43 @@ pub fn process_account(
|
||||||
let sig = mango_client
|
let sig = mango_client
|
||||||
.liq_token_with_token(
|
.liq_token_with_token(
|
||||||
(pubkey, &account),
|
(pubkey, &account),
|
||||||
*asset_token_index,
|
asset_token_index,
|
||||||
*liab_token_index,
|
liab_token_index,
|
||||||
max_liab_transfer,
|
max_liab_transfer,
|
||||||
)
|
)
|
||||||
.context("sending liq_token_with_token")?;
|
.context("sending liq_token_with_token")?;
|
||||||
log::info!(
|
log::info!(
|
||||||
"Liquidated token with token for {}..., maint_health was {}, tx sig {:?}",
|
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
|
||||||
&pubkey.to_string()[..3],
|
pubkey,
|
||||||
maint_health,
|
maint_health,
|
||||||
sig
|
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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn process_accounts<'a>(
|
pub fn maybe_liquidate_one<'a>(
|
||||||
mango_client: &MangoClient,
|
mango_client: &MangoClient,
|
||||||
account_fetcher: &chain_data::AccountFetcher,
|
account_fetcher: &chain_data::AccountFetcher,
|
||||||
accounts: impl Iterator<Item = &'a Pubkey>,
|
accounts: impl Iterator<Item = &'a Pubkey>,
|
||||||
) -> anyhow::Result<()> {
|
config: &Config,
|
||||||
|
) -> bool {
|
||||||
for pubkey in accounts {
|
for pubkey in accounts {
|
||||||
match process_account(mango_client, account_fetcher, pubkey) {
|
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config) {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Not all errors need to be raised to the user's attention.
|
// Not all errors need to be raised to the user's attention.
|
||||||
let mut log_level = log::Level::Error;
|
let mut log_level = log::Level::Error;
|
||||||
|
@ -185,9 +298,10 @@ pub fn process_accounts<'a>(
|
||||||
};
|
};
|
||||||
log::log!(log_level, "liquidating account {}: {:?}", pubkey, err);
|
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 account_shared_data;
|
||||||
pub mod liquidate;
|
pub mod liquidate;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
|
pub mod rebalance;
|
||||||
pub mod snapshot_source;
|
pub mod snapshot_source;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod websocket_source;
|
pub mod websocket_source;
|
||||||
|
@ -67,13 +68,20 @@ struct Cli {
|
||||||
#[clap(long, env, default_value = "300")]
|
#[clap(long, env, default_value = "300")]
|
||||||
snapshot_interval_secs: u64,
|
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")]
|
#[clap(long, env, default_value = "10")]
|
||||||
parallel_rpc_requests: usize,
|
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")]
|
#[clap(long, env, default_value = "100")]
|
||||||
get_multiple_accounts_count: usize,
|
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 {
|
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)?;
|
let group_context = MangoGroupContext::new_from_rpc(mango_group, cluster.clone(), commitment)?;
|
||||||
|
|
||||||
// TODO: this is all oracles, not just pyth!
|
let mango_oracles = group_context
|
||||||
let mango_pyth_oracles = group_context
|
|
||||||
.tokens
|
.tokens
|
||||||
.values()
|
.values()
|
||||||
.map(|value| value.mint_info.oracle)
|
.map(|value| value.mint_info.oracle)
|
||||||
|
@ -145,7 +152,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
serum_program: cli.serum_program,
|
serum_program: cli.serum_program,
|
||||||
open_orders_authority: mango_group,
|
open_orders_authority: mango_group,
|
||||||
},
|
},
|
||||||
mango_pyth_oracles.clone(),
|
mango_oracles.clone(),
|
||||||
websocket_sender,
|
websocket_sender,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -169,7 +176,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
snapshot_interval: std::time::Duration::from_secs(cli.snapshot_interval_secs),
|
snapshot_interval: std::time::Duration::from_secs(cli.snapshot_interval_secs),
|
||||||
min_slot: first_websocket_slot + 10,
|
min_slot: first_websocket_slot + 10,
|
||||||
},
|
},
|
||||||
mango_pyth_oracles,
|
mango_oracles,
|
||||||
snapshot_sender,
|
snapshot_sender,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -183,14 +190,6 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let mut oracles = HashSet::<Pubkey>::new();
|
let mut oracles = HashSet::<Pubkey>::new();
|
||||||
let mut perp_markets = HashMap::<PerpMarketIndex, 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.
|
// Is the first snapshot done? Only start checking account health when it is.
|
||||||
let mut one_snapshot_done = false;
|
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");
|
info!("main loop");
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
@ -238,14 +250,13 @@ async fn main() -> anyhow::Result<()> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = liquidate::process_accounts(
|
liquidate(
|
||||||
&mango_client,
|
&mango_client,
|
||||||
&account_fetcher,
|
&account_fetcher,
|
||||||
std::iter::once(&account_write.pubkey),
|
std::iter::once(&account_write.pubkey),
|
||||||
|
&liq_config,
|
||||||
) {
|
&rebalance_config,
|
||||||
warn!("could not process account {}: {:?}", account_write.pubkey, err);
|
)?;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_mango_bank(&account_write.account, &mango_program, &mango_group).is_some() || oracles.contains(&account_write.pubkey) {
|
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);
|
log::debug!("change to oracle {}", &account_write.pubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check health of all accounts
|
liquidate(
|
||||||
//
|
&mango_client,
|
||||||
// TODO: This could be done asynchronously by calling
|
&account_fetcher,
|
||||||
// let accounts = chain_data.accounts_snapshot();
|
mango_accounts.iter(),
|
||||||
// and then working with the snapshot of the data
|
&liq_config,
|
||||||
//
|
&rebalance_config,
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -302,19 +305,46 @@ async fn main() -> anyhow::Result<()> {
|
||||||
snapshot_source::update_chain_data(&mut chain_data.write().unwrap(), message);
|
snapshot_source::update_chain_data(&mut chain_data.write().unwrap(), message);
|
||||||
one_snapshot_done = true;
|
one_snapshot_done = true;
|
||||||
|
|
||||||
// trigger a full health check
|
liquidate(
|
||||||
if let Err(err) = liquidate::process_accounts(
|
&mango_client,
|
||||||
&mango_client,
|
&account_fetcher,
|
||||||
&account_fetcher,
|
mango_accounts.iter(),
|
||||||
mango_accounts.iter(),
|
&liq_config,
|
||||||
) {
|
&rebalance_config,
|
||||||
warn!("could not process accounts: {:?}", err);
|
)?;
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_ = 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) {
|
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));
|
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(
|
async fn feed_snapshots(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
mango_pyth_oracles: Vec<Pubkey>,
|
mango_oracles: Vec<Pubkey>,
|
||||||
sender: &async_channel::Sender<AccountSnapshot>,
|
sender: &async_channel::Sender<AccountSnapshot>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let rpc_client = http::connect_with_options::<AccountsDataClient>(&config.rpc_http_url, true)
|
let rpc_client = http::connect_with_options::<AccountsDataClient>(&config.rpc_http_url, true)
|
||||||
|
@ -128,7 +128,7 @@ async fn feed_snapshots(
|
||||||
let results: Vec<(
|
let results: Vec<(
|
||||||
Vec<Pubkey>,
|
Vec<Pubkey>,
|
||||||
Result<Response<Vec<Option<UiAccount>>>, jsonrpc_core_client::RpcError>,
|
Result<Response<Vec<Option<UiAccount>>>, jsonrpc_core_client::RpcError>,
|
||||||
)> = stream::iter(mango_pyth_oracles)
|
)> = stream::iter(mango_oracles)
|
||||||
.chunks(config.get_multiple_accounts_count)
|
.chunks(config.get_multiple_accounts_count)
|
||||||
.map(|keys| {
|
.map(|keys| {
|
||||||
let rpc_client = &rpc_client;
|
let rpc_client = &rpc_client;
|
||||||
|
@ -207,7 +207,7 @@ async fn feed_snapshots(
|
||||||
|
|
||||||
pub fn start(
|
pub fn start(
|
||||||
config: Config,
|
config: Config,
|
||||||
mango_pyth_oracles: Vec<Pubkey>,
|
mango_oracles: Vec<Pubkey>,
|
||||||
sender: async_channel::Sender<AccountSnapshot>,
|
sender: async_channel::Sender<AccountSnapshot>,
|
||||||
) {
|
) {
|
||||||
let mut poll_wait_first_snapshot = time::interval(time::Duration::from_secs(2));
|
let mut poll_wait_first_snapshot = time::interval(time::Duration::from_secs(2));
|
||||||
|
@ -239,7 +239,7 @@ pub fn start(
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval_between_snapshots.tick().await;
|
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);
|
warn!("snapshot error: {:?}", err);
|
||||||
} else {
|
} else {
|
||||||
info!("snapshot success");
|
info!("snapshot success");
|
||||||
|
|
|
@ -57,7 +57,7 @@ pub struct Config {
|
||||||
|
|
||||||
async fn feed_data(
|
async fn feed_data(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
mango_pyth_oracles: Vec<Pubkey>,
|
mango_oracles: Vec<Pubkey>,
|
||||||
sender: async_channel::Sender<Message>,
|
sender: async_channel::Sender<Message>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let connect = ws::try_connect::<RpcSolPubSubClient>(&config.rpc_ws_url).map_err_anyhow()?;
|
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()),
|
Some(all_accounts_config.clone()),
|
||||||
)
|
)
|
||||||
.map_err_anyhow()?;
|
.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_oracles_sub_map = StreamMap::new();
|
||||||
let mut mango_pyth_oracles_sub_map = StreamMap::new();
|
for oracle in mango_oracles.into_iter() {
|
||||||
for oracle in mango_pyth_oracles.into_iter() {
|
mango_oracles_sub_map.insert(
|
||||||
mango_pyth_oracles_sub_map.insert(
|
|
||||||
oracle,
|
oracle,
|
||||||
client
|
client
|
||||||
.account_subscribe(
|
.account_subscribe(
|
||||||
|
@ -136,13 +135,13 @@ async fn feed_data(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
message = mango_pyth_oracles_sub_map.next() => {
|
message = mango_oracles_sub_map.next() => {
|
||||||
if let Some(data) = message {
|
if let Some(data) = message {
|
||||||
let response = data.1.map_err_anyhow()?;
|
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} } ;
|
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");
|
sender.send(Message::Account(AccountUpdate::from_rpc(response)?)).await.expect("sending must succeed");
|
||||||
} else {
|
} else {
|
||||||
warn!("pyth stream closed");
|
warn!("oracle stream closed");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -171,16 +170,12 @@ async fn feed_data(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(
|
pub fn start(config: Config, mango_oracles: Vec<Pubkey>, sender: async_channel::Sender<Message>) {
|
||||||
config: Config,
|
|
||||||
mango_pyth_oracles: Vec<Pubkey>,
|
|
||||||
sender: async_channel::Sender<Message>,
|
|
||||||
) {
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// if the websocket disconnects, we get no data in a while etc, reconnect and try again
|
// if the websocket disconnects, we get no data in a while etc, reconnect and try again
|
||||||
loop {
|
loop {
|
||||||
info!("connecting to solana websocket streams");
|
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;
|
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
|
// 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.
|
// 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_asset_position, liqee_asset_raw_index) = liqee.token_get(asset_token_index)?;
|
||||||
let liqee_assets_native = liqee_asset_position.native(asset_bank);
|
let liqee_asset_native = liqee_asset_position.native(asset_bank);
|
||||||
require!(liqee_assets_native.is_positive(), MangoError::SomeError);
|
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_position, liqee_liab_raw_index) = liqee.token_get(liab_token_index)?;
|
||||||
let liqee_liab_native = liqee_liab_position.native(liab_bank);
|
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));
|
/ (liab_price * init_liab_weight - init_asset_weight * liab_price_adjusted));
|
||||||
|
|
||||||
// How much liab can we get at most for the asset balance?
|
// 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
|
// The amount of liab native tokens we will transfer
|
||||||
let liab_transfer = min(
|
let liab_transfer = min(
|
||||||
|
@ -135,6 +135,7 @@ pub fn liq_token_with_token(
|
||||||
liqor.token_get_mut_or_create(liab_token_index)?;
|
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_active = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?;
|
||||||
let liqor_liab_position_indexed = liqor_liab_position.indexed_position;
|
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, _) =
|
let (liqor_asset_position, liqor_asset_raw_index, _) =
|
||||||
liqor.token_get_mut_or_create(asset_token_index)?;
|
liqor.token_get_mut_or_create(asset_token_index)?;
|
||||||
|
@ -145,10 +146,17 @@ pub fn liq_token_with_token(
|
||||||
let liqee_asset_active =
|
let liqee_asset_active =
|
||||||
asset_bank.withdraw_without_fee(liqee_asset_position, asset_transfer)?;
|
asset_bank.withdraw_without_fee(liqee_asset_position, asset_transfer)?;
|
||||||
let liqee_asset_position_indexed = liqee_asset_position.indexed_position;
|
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
|
// Update the health cache
|
||||||
liqee_health_cache.adjust_token_balance(liab_token_index, liab_transfer)?;
|
liqee_health_cache.adjust_token_balance(
|
||||||
liqee_health_cache.adjust_token_balance(asset_token_index, -asset_transfer)?;
|
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!(
|
msg!(
|
||||||
"liquidated {} liab for {} asset",
|
"liquidated {} liab for {} asset",
|
||||||
|
|
|
@ -505,5 +505,73 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
||||||
assert!(!liqee.being_liquidated());
|
assert!(!liqee.being_liquidated());
|
||||||
assert!(!liqee.is_bankrupt());
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,10 +128,7 @@ export class MangoClient {
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getGroupForCreator(
|
public async getGroupsForCreator(creatorPk: PublicKey): Promise<Group[]> {
|
||||||
creatorPk: PublicKey,
|
|
||||||
groupNum?: number,
|
|
||||||
): Promise<Group> {
|
|
||||||
const filters: MemcmpFilter[] = [
|
const filters: MemcmpFilter[] = [
|
||||||
{
|
{
|
||||||
memcmp: {
|
memcmp: {
|
||||||
|
@ -141,20 +138,25 @@ export class MangoClient {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (groupNum !== undefined) {
|
return (await this.program.account.group.all(filters)).map((tuple) =>
|
||||||
const bbuf = Buffer.alloc(4);
|
Group.from(tuple.publicKey, tuple.account),
|
||||||
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),
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
await groups[0].reloadAll(this);
|
||||||
return groups[0];
|
return groups[0];
|
||||||
}
|
}
|
||||||
|
@ -472,12 +474,35 @@ export class MangoClient {
|
||||||
accountNumber?: number,
|
accountNumber?: number,
|
||||||
name?: string,
|
name?: string,
|
||||||
): Promise<MangoAccount> {
|
): Promise<MangoAccount> {
|
||||||
let mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk);
|
// TODO: this function discards accountSize and name when the account exists already!
|
||||||
if (mangoAccounts.length === 0) {
|
// TODO: this function always creates accounts for this.program.owner, and not
|
||||||
await this.createMangoAccount(group, accountNumber, name);
|
// ownerPk! It needs to get passed a keypair, and we need to add
|
||||||
mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk);
|
// 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(
|
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(
|
public async getMangoAccountsForOwner(
|
||||||
group: Group,
|
group: Group,
|
||||||
ownerPk: PublicKey,
|
ownerPk: PublicKey,
|
||||||
|
@ -623,6 +658,22 @@ export class MangoClient {
|
||||||
amount: number,
|
amount: number,
|
||||||
): Promise<TransactionSignature> {
|
): Promise<TransactionSignature> {
|
||||||
const bank = group.banksMap.get(tokenName)!;
|
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(
|
const tokenAccountPk = await getAssociatedTokenAddress(
|
||||||
bank.mint,
|
bank.mint,
|
||||||
|
@ -635,7 +686,7 @@ export class MangoClient {
|
||||||
const additionalSigners: Signer[] = [];
|
const additionalSigners: Signer[] = [];
|
||||||
if (bank.mint.equals(WRAPPED_SOL_MINT)) {
|
if (bank.mint.equals(WRAPPED_SOL_MINT)) {
|
||||||
wrappedSolAccount = new Keypair();
|
wrappedSolAccount = new Keypair();
|
||||||
const lamports = Math.round(amount * LAMPORTS_PER_SOL) + 1e7;
|
const lamports = nativeAmount + 1e7;
|
||||||
|
|
||||||
preInstructions = [
|
preInstructions = [
|
||||||
SystemProgram.createAccount({
|
SystemProgram.createAccount({
|
||||||
|
@ -670,7 +721,7 @@ export class MangoClient {
|
||||||
);
|
);
|
||||||
|
|
||||||
return await this.program.methods
|
return await this.program.methods
|
||||||
.tokenDeposit(toNativeDecimals(amount, bank.mintDecimals))
|
.tokenDeposit(new BN(nativeAmount))
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
account: mangoAccount.publicKey,
|
account: mangoAccount.publicKey,
|
||||||
|
@ -699,45 +750,14 @@ export class MangoClient {
|
||||||
allowBorrow: boolean,
|
allowBorrow: boolean,
|
||||||
): Promise<TransactionSignature> {
|
): Promise<TransactionSignature> {
|
||||||
const bank = group.banksMap.get(tokenName)!;
|
const bank = group.banksMap.get(tokenName)!;
|
||||||
|
const nativeAmount = toNativeDecimals(amount, bank.mintDecimals).toNumber();
|
||||||
const tokenAccountPk = await getAssociatedTokenAddress(
|
return await this.tokenWithdrawNative(
|
||||||
bank.mint,
|
group,
|
||||||
mangoAccount.owner,
|
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(
|
public async tokenWithdrawNative(
|
||||||
|
@ -778,6 +798,14 @@ export class MangoClient {
|
||||||
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
|
({ 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 });
|
.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
|
// example script to close accounts - banks, markets, group etc. which require admin to be the signer
|
||||||
//
|
//
|
||||||
|
|
||||||
const MAINNET_MINTS = new Map([
|
// Use to close only a specific group by number. Use "all" to close all groups.
|
||||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
const GROUP_NUM = process.env.GROUP_NUM;
|
||||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
|
||||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const options = AnchorProvider.defaultOptions();
|
const options = AnchorProvider.defaultOptions();
|
||||||
|
@ -32,49 +29,58 @@ async function main() {
|
||||||
MANGO_V4_ID['mainnet-beta'],
|
MANGO_V4_ID['mainnet-beta'],
|
||||||
);
|
);
|
||||||
|
|
||||||
const group = await client.getGroupForCreator(admin.publicKey);
|
const groups = await (async () => {
|
||||||
console.log(`Group ${group.publicKey}`);
|
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
|
// close stub oracles
|
||||||
const usdcMainnetBetaMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
const stubOracles = await client.getStubOracle(group);
|
||||||
const usdcMainnetBetaOracle = (
|
for (const stubOracle of stubOracles) {
|
||||||
await client.getStubOracle(group, usdcMainnetBetaMint)
|
sig = await client.stubOracleClose(group, stubOracle.publicKey);
|
||||||
)[0];
|
console.log(
|
||||||
sig = await client.stubOracleClose(group, usdcMainnetBetaOracle.publicKey);
|
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`,
|
||||||
console.log(
|
);
|
||||||
`Closed USDC stub oracle, sig https://explorer.solana.com/tx/${sig}`,
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// close all bank
|
// close all banks
|
||||||
for (const bank of group.banksMap.values()) {
|
for (const bank of group.banksMap.values()) {
|
||||||
sig = await client.tokenDeregister(group, bank.name);
|
sig = await client.tokenDeregister(group, bank.name);
|
||||||
console.log(
|
console.log(
|
||||||
`Removed token ${bank.name}, sig https://explorer.solana.com/tx/${sig}`,
|
`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();
|
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