Merge branch 'main' into deploy
This commit is contained in:
commit
f2442428b6
|
@ -23,7 +23,7 @@ jobs:
|
|||
|
||||
- name: Verifiable Build
|
||||
run: |
|
||||
anchor build --verifiable --docker-image backpackapp/build:v0.28.0 --solana-version 1.16.14 -- --features enable-gpl
|
||||
anchor build --verifiable --docker-image backpackapp/build:v0.28.0 --solana-version 1.16.14 --env GITHUB_SHA --env GITHUB_REF_NAME -- --features enable-gpl
|
||||
|
||||
- name: Generate Checksum
|
||||
run: |
|
||||
|
|
56
CHANGELOG.md
56
CHANGELOG.md
|
@ -4,6 +4,7 @@ Update this for each program release and mainnet deployment.
|
|||
|
||||
## not on mainnet
|
||||
|
||||
<<<<<<< HEAD
|
||||
### v0.22.0, 2024-2-
|
||||
|
||||
- Perp: Allow reusing your own perp order slots immediately (#817)
|
||||
|
@ -32,6 +33,61 @@ Update this for each program release and mainnet deployment.
|
|||
|
||||
## mainnet
|
||||
|
||||
=======
|
||||
### v0.23.0, 2024-3-
|
||||
|
||||
- Allow disabling asset liquidations for tokens (#867)
|
||||
|
||||
This allows listing tokens that have no reliable oracle. Those tokens could be
|
||||
traded through mango but can't be borrowed, can't have asset weight and can't
|
||||
even be liquidated.
|
||||
|
||||
- Add configurable collateral fees for tokens (#868, #880, #894)
|
||||
|
||||
Collateral fees allow the DAO to regularly charge users for using particular
|
||||
types of collateral to back their liabilities.
|
||||
|
||||
- Add force_withdraw token state (#884)
|
||||
|
||||
There already is a force_close_borrows state, but for a full delisting user
|
||||
deposits need to be removed too. In force_withdraw, user deposits can be
|
||||
permissionlessly withdrawn to their owners' token accounts.
|
||||
|
||||
- Flash loan: Add a "swap without flash loan fees" option (#882)
|
||||
- Cleanup, tests and minor (#878, #875, #854, #838, #895)
|
||||
|
||||
## mainnet
|
||||
|
||||
### v0.22.0, 2024-3-3
|
||||
|
||||
Deployment: Mar 3, 2024 at 23:52:08 Central European Standard Time, https://explorer.solana.com/tx/3MpEMU12Pv7RpSnwfShoM9sbyr41KAEeJFCVx9ypkq8nuK8Q5vm7CRLkdhH3u91yQ4k44a32armZHaoYguX6NqsY
|
||||
|
||||
- Perp: Allow reusing your own perp order slots immediately (#817)
|
||||
|
||||
Previously users who placed a lot of perp orders and used time-in-force needed
|
||||
to wait for out-event cranking if their perp order before reusing an order
|
||||
slot. Now perp order slots can be reused even when the out-event is still on
|
||||
the event queue.
|
||||
|
||||
- Introduce fallback oracles (#790, #813)
|
||||
|
||||
Fallback oracles can be used when the primary oracle is stale or not confident.
|
||||
These oracles need to configured by the DAO to be usable by clients.
|
||||
|
||||
Fallback oracles may be based on Orca in addition to the other supported types.
|
||||
|
||||
- Add serum3_cancel_by_client_order_id instruction (#798)
|
||||
|
||||
Can now cancel by client order id and not just the order id.
|
||||
|
||||
- Add configurable platform liquidation fees for tokens and perps (#849, #858)
|
||||
- Delegates can now withdraw small token amounts to the owner's ata (#820)
|
||||
- Custom allocator to allow larger heap use if needed (#801)
|
||||
- Optimize compute use in token_deposit instruction (#786)
|
||||
- Disable support for v1 and v2 mango accounts (#783)
|
||||
- Cleanups, logging and tests (#819, #799, #818, #823, #834, #828, #833)
|
||||
|
||||
>>>>>>> main
|
||||
### v0.21.3, 2024-2-9
|
||||
|
||||
Deployment: Feb 9, 2024 at 11:21:58 Central European Standard Time, https://explorer.solana.com/tx/44f2wcLyLiic1aycdaPTdfwXJBMeGeuA984kvCByg4L5iGprH6xW3D35gd3bvZ6kU3SipEtoY3kDuexJghbxL89T
|
||||
|
|
|
@ -3367,7 +3367,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mango-v4"
|
||||
<<<<<<< HEAD
|
||||
version = "0.22.0"
|
||||
=======
|
||||
version = "0.23.0"
|
||||
>>>>>>> main
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"anchor-spl",
|
||||
|
@ -3445,6 +3449,7 @@ dependencies = [
|
|||
"atty",
|
||||
"base64 0.13.1",
|
||||
"bincode",
|
||||
"clap 3.2.25",
|
||||
"derive_builder",
|
||||
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
|
||||
"futures 0.3.28",
|
||||
|
@ -3464,10 +3469,12 @@ dependencies = [
|
|||
"solana-client",
|
||||
"solana-rpc",
|
||||
"solana-sdk",
|
||||
"solana-transaction-status",
|
||||
"spl-associated-token-account 1.1.3",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite 0.17.2",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
@ -3528,6 +3535,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"pyth-sdk-solana",
|
||||
"rand 0.7.3",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
|
|
@ -24,6 +24,7 @@ solana-program = "~1.16.7"
|
|||
solana-program-test = "~1.16.7"
|
||||
solana-rpc = "~1.16.7"
|
||||
solana-sdk = { version = "~1.16.7", default-features = false }
|
||||
solana-transaction-status = { version = "~1.16.7" }
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = true
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
|
||||
- Do a verifiable build
|
||||
|
||||
anchor build --verifiable --solana-version 1.14.13 -- --features enable-gpl
|
||||
Set GITHUB_SHA and GITHUB_REF_NAME to the release sha1 and tag name.
|
||||
|
||||
anchor build --verifiable --docker-image backpackapp/build:v0.28.0 --solana-version 1.16.14 --env GITHUB_SHA --env GITHUB_REF_NAME -- --features enable-gpl
|
||||
|
||||
(or wait for github to finish and create the release)
|
||||
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
use clap::clap_derive::ArgEnum;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
|
||||
use mango_v4::state::{PlaceOrderType, SelfTradeBehavior, Side};
|
||||
use mango_v4_client::{
|
||||
keypair_from_cli, pubkey_from_cli, Client, MangoClient, TransactionBuilderConfig,
|
||||
};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
mod save_snapshot;
|
||||
mod test_oracles;
|
||||
|
@ -88,6 +92,98 @@ struct JupiterSwap {
|
|||
rpc: Rpc,
|
||||
}
|
||||
|
||||
#[derive(ArgEnum, Clone, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum CliSide {
|
||||
Bid = 0,
|
||||
Ask = 1,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
struct PerpPlaceOrder {
|
||||
#[clap(long)]
|
||||
account: String,
|
||||
|
||||
/// also pays for everything
|
||||
#[clap(short, long)]
|
||||
owner: String,
|
||||
|
||||
#[clap(long)]
|
||||
market_name: String,
|
||||
|
||||
#[clap(long, value_enum)]
|
||||
side: CliSide,
|
||||
|
||||
#[clap(short, long)]
|
||||
price: f64,
|
||||
|
||||
#[clap(long)]
|
||||
quantity: f64,
|
||||
|
||||
#[clap(long)]
|
||||
expiry: u64,
|
||||
|
||||
#[clap(flatten)]
|
||||
rpc: Rpc,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
struct Serum3CreateOpenOrders {
|
||||
#[clap(long)]
|
||||
account: String,
|
||||
|
||||
/// also pays for everything
|
||||
#[clap(short, long)]
|
||||
owner: String,
|
||||
|
||||
#[clap(long)]
|
||||
market_name: String,
|
||||
|
||||
#[clap(flatten)]
|
||||
rpc: Rpc,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
struct Serum3CloseOpenOrders {
|
||||
#[clap(long)]
|
||||
account: String,
|
||||
|
||||
/// also pays for everything
|
||||
#[clap(short, long)]
|
||||
owner: String,
|
||||
|
||||
#[clap(long)]
|
||||
market_name: String,
|
||||
|
||||
#[clap(flatten)]
|
||||
rpc: Rpc,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
struct Serum3PlaceOrder {
|
||||
#[clap(long)]
|
||||
account: String,
|
||||
|
||||
/// also pays for everything
|
||||
#[clap(short, long)]
|
||||
owner: String,
|
||||
|
||||
#[clap(long)]
|
||||
market_name: String,
|
||||
|
||||
#[clap(long, value_enum)]
|
||||
side: CliSide,
|
||||
|
||||
#[clap(short, long)]
|
||||
price: f64,
|
||||
|
||||
#[clap(long)]
|
||||
quantity: f64,
|
||||
|
||||
#[clap(flatten)]
|
||||
rpc: Rpc,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
enum Command {
|
||||
CreateAccount(CreateAccount),
|
||||
|
@ -128,21 +224,28 @@ enum Command {
|
|||
#[clap(short, long)]
|
||||
output: String,
|
||||
},
|
||||
PerpPlaceOrder(PerpPlaceOrder),
|
||||
Serum3CloseOpenOrders(Serum3CloseOpenOrders),
|
||||
Serum3CreateOpenOrders(Serum3CreateOpenOrders),
|
||||
Serum3PlaceOrder(Serum3PlaceOrder),
|
||||
}
|
||||
|
||||
impl Rpc {
|
||||
fn client(&self, override_fee_payer: Option<&str>) -> anyhow::Result<Client> {
|
||||
let fee_payer = keypair_from_cli(override_fee_payer.unwrap_or(&self.fee_payer));
|
||||
Ok(Client::new(
|
||||
anchor_client::Cluster::from_str(&self.url)?,
|
||||
solana_sdk::commitment_config::CommitmentConfig::confirmed(),
|
||||
Arc::new(fee_payer),
|
||||
None,
|
||||
TransactionBuilderConfig {
|
||||
prioritization_micro_lamports: Some(5),
|
||||
compute_budget_per_instruction: Some(250_000),
|
||||
},
|
||||
))
|
||||
Ok(Client::builder()
|
||||
.cluster(anchor_client::Cluster::from_str(&self.url)?)
|
||||
.commitment(solana_sdk::commitment_config::CommitmentConfig::confirmed())
|
||||
.fee_payer(Some(Arc::new(fee_payer)))
|
||||
.transaction_builder_config(
|
||||
TransactionBuilderConfig::builder()
|
||||
.prioritization_micro_lamports(Some(5))
|
||||
.compute_budget_per_instruction(Some(250_000))
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.build()
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,15 +307,8 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
let output_mint = pubkey_from_cli(&cmd.output_mint);
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||
let txsig = client
|
||||
.jupiter_v4()
|
||||
.swap(
|
||||
input_mint,
|
||||
output_mint,
|
||||
cmd.amount,
|
||||
cmd.slippage_bps,
|
||||
mango_v4_client::JupiterSwapMode::ExactIn,
|
||||
false,
|
||||
)
|
||||
.jupiter_v6()
|
||||
.swap(input_mint, output_mint, cmd.amount, cmd.slippage_bps, false)
|
||||
.await?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
|
@ -245,6 +341,111 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
let client = rpc.client(None)?;
|
||||
save_snapshot::save_snapshot(mango_group, client, output).await?
|
||||
}
|
||||
Command::PerpPlaceOrder(cmd) => {
|
||||
let client = cmd.rpc.client(Some(&cmd.owner))?;
|
||||
let account = pubkey_from_cli(&cmd.account);
|
||||
let owner = Arc::new(keypair_from_cli(&cmd.owner));
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||
let market = client
|
||||
.context
|
||||
.perp_markets
|
||||
.iter()
|
||||
.find(|p| p.1.name == cmd.market_name)
|
||||
.unwrap()
|
||||
.1;
|
||||
|
||||
fn native(x: f64, b: u32) -> i64 {
|
||||
(x * (10_i64.pow(b)) as f64) as i64
|
||||
}
|
||||
|
||||
let price_lots = native(cmd.price, 6) * market.base_lot_size
|
||||
/ (market.quote_lot_size * 10_i64.pow(market.base_decimals.into()));
|
||||
let max_base_lots =
|
||||
native(cmd.quantity, market.base_decimals.into()) / market.base_lot_size;
|
||||
|
||||
let txsig = client
|
||||
.perp_place_order(
|
||||
market.perp_market_index,
|
||||
match cmd.side {
|
||||
CliSide::Bid => Side::Bid,
|
||||
CliSide::Ask => Side::Ask,
|
||||
},
|
||||
price_lots,
|
||||
max_base_lots,
|
||||
i64::max_value(),
|
||||
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
|
||||
PlaceOrderType::Limit,
|
||||
false,
|
||||
if cmd.expiry > 0 {
|
||||
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + cmd.expiry
|
||||
} else {
|
||||
0
|
||||
},
|
||||
10,
|
||||
SelfTradeBehavior::AbortTransaction,
|
||||
)
|
||||
.await?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::Serum3CreateOpenOrders(cmd) => {
|
||||
let client = cmd.rpc.client(Some(&cmd.owner))?;
|
||||
let account = pubkey_from_cli(&cmd.account);
|
||||
let owner = Arc::new(keypair_from_cli(&cmd.owner));
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||
|
||||
let txsig = client.serum3_create_open_orders(&cmd.market_name).await?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::Serum3CloseOpenOrders(cmd) => {
|
||||
let client = cmd.rpc.client(Some(&cmd.owner))?;
|
||||
let account = pubkey_from_cli(&cmd.account);
|
||||
let owner = Arc::new(keypair_from_cli(&cmd.owner));
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||
|
||||
let txsig = client.serum3_close_open_orders(&cmd.market_name).await?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::Serum3PlaceOrder(cmd) => {
|
||||
let client = cmd.rpc.client(Some(&cmd.owner))?;
|
||||
let account = pubkey_from_cli(&cmd.account);
|
||||
let owner = Arc::new(keypair_from_cli(&cmd.owner));
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||
let market_index = client.context.serum3_market_index(&cmd.market_name);
|
||||
let market = client.context.serum3(market_index);
|
||||
let base_token = client.context.token(market.base_token_index);
|
||||
let quote_token = client.context.token(market.quote_token_index);
|
||||
|
||||
fn native(x: f64, b: u32) -> u64 {
|
||||
(x * (10_i64.pow(b)) as f64) as u64
|
||||
}
|
||||
|
||||
// coin_lot_size = base lot size ?
|
||||
// cf priceNumberToLots
|
||||
let price_lots = native(cmd.price, quote_token.decimals as u32) * market.coin_lot_size
|
||||
/ (native(1.0, base_token.decimals as u32) * market.pc_lot_size);
|
||||
|
||||
// cf baseSizeNumberToLots
|
||||
let max_base_lots =
|
||||
native(cmd.quantity, base_token.decimals as u32) / market.coin_lot_size;
|
||||
|
||||
let txsig = client
|
||||
.serum3_place_order(
|
||||
&cmd.market_name,
|
||||
match cmd.side {
|
||||
CliSide::Bid => Serum3Side::Bid,
|
||||
CliSide::Ask => Serum3Side::Ask,
|
||||
},
|
||||
price_lots,
|
||||
max_base_lots as u64,
|
||||
((price_lots * max_base_lots) as f64 * 1.01) as u64,
|
||||
Serum3SelfTradeBehavior::AbortTransaction,
|
||||
Serum3OrderType::Limit,
|
||||
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
|
||||
10,
|
||||
)
|
||||
.await?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -23,10 +23,10 @@ pub async fn save_snapshot(
|
|||
}
|
||||
fs::create_dir_all(out_path).unwrap();
|
||||
|
||||
let rpc_url = client.cluster.url().to_string();
|
||||
let ws_url = client.cluster.ws_url().to_string();
|
||||
let rpc_url = client.config().cluster.url().to_string();
|
||||
let ws_url = client.config().cluster.ws_url().to_string();
|
||||
|
||||
let group_context = MangoGroupContext::new_from_rpc(&client.rpc_async(), mango_group).await?;
|
||||
let group_context = MangoGroupContext::new_from_rpc(client.rpc_async(), mango_group).await?;
|
||||
|
||||
let oracles_and_vaults = group_context
|
||||
.tokens
|
||||
|
|
|
@ -1,17 +1,34 @@
|
|||
use std::{collections::HashSet, sync::Arc, time::Duration, time::Instant};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use crate::MangoClient;
|
||||
use anyhow::Context;
|
||||
use itertools::Itertools;
|
||||
|
||||
use anchor_lang::{__private::bytemuck::cast_ref, solana_program};
|
||||
use anchor_lang::{__private::bytemuck::cast_ref, solana_program, Discriminator};
|
||||
use futures::Future;
|
||||
use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, TokenIndex};
|
||||
use mango_v4_client::PerpMarketContext;
|
||||
use mango_v4::{
|
||||
accounts_zerocopy::AccountReader,
|
||||
state::{
|
||||
EventQueue, EventType, FillEvent, Group, MangoAccount, MangoAccountValue, OutEvent,
|
||||
TokenIndex,
|
||||
},
|
||||
};
|
||||
use mango_v4_client::{
|
||||
account_fetcher_fetch_anchor_account, AccountFetcher, PerpMarketContext, PreparedInstructions,
|
||||
RpcAccountFetcher, TransactionBuilder,
|
||||
};
|
||||
use prometheus::{register_histogram, Encoder, Histogram, IntCounter, Registry};
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
signature::Signature,
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::*;
|
||||
use warp::Filter;
|
||||
|
||||
|
@ -80,6 +97,9 @@ pub async fn runner(
|
|||
interval_consume_events: u64,
|
||||
interval_update_funding: u64,
|
||||
interval_check_for_changes_and_abort: u64,
|
||||
interval_charge_collateral_fees: u64,
|
||||
max_cu_when_batching: u32,
|
||||
extra_jobs: Vec<JoinHandle<()>>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let handles1 = mango_client
|
||||
.context
|
||||
|
@ -138,12 +158,18 @@ pub async fn runner(
|
|||
futures::future::join_all(handles1),
|
||||
futures::future::join_all(handles2),
|
||||
futures::future::join_all(handles3),
|
||||
loop_charge_collateral_fees(
|
||||
mango_client.clone(),
|
||||
interval_charge_collateral_fees,
|
||||
max_cu_when_batching
|
||||
),
|
||||
MangoClient::loop_check_for_context_changes_and_abort(
|
||||
mango_client.clone(),
|
||||
Duration::from_secs(interval_check_for_changes_and_abort),
|
||||
),
|
||||
serve_metrics(),
|
||||
debugging_handle,
|
||||
futures::future::join_all(extra_jobs),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
@ -409,3 +435,146 @@ pub async fn loop_update_funding(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn loop_charge_collateral_fees(
|
||||
mango_client: Arc<MangoClient>,
|
||||
interval: u64,
|
||||
max_cu_when_batching: u32,
|
||||
) {
|
||||
if interval == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make a new one separate from the mango_client.account_fetcher,
|
||||
// because we don't want cached responses
|
||||
let fetcher = RpcAccountFetcher {
|
||||
rpc: mango_client.client.new_rpc_async(),
|
||||
};
|
||||
|
||||
let group: Group = account_fetcher_fetch_anchor_account(&fetcher, &mango_client.context.group)
|
||||
.await
|
||||
.unwrap();
|
||||
let collateral_fee_interval = group.collateral_fee_interval;
|
||||
|
||||
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(interval));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
match charge_collateral_fees_inner(
|
||||
&mango_client,
|
||||
&fetcher,
|
||||
collateral_fee_interval,
|
||||
max_cu_when_batching,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
error!("charge_collateral_fees error: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn charge_collateral_fees_inner(
|
||||
client: &MangoClient,
|
||||
fetcher: &RpcAccountFetcher,
|
||||
collateral_fee_interval: u64,
|
||||
max_cu_when_batching: u32,
|
||||
) -> anyhow::Result<()> {
|
||||
let mango_accounts = fetcher
|
||||
.fetch_program_accounts(&mango_v4::id(), MangoAccount::DISCRIMINATOR)
|
||||
.await
|
||||
.context("fetching mango accounts")?
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(pk, data)| match MangoAccountValue::from_bytes(&data.data()[8..]) {
|
||||
Ok(acc) => Some((pk, acc)),
|
||||
Err(err) => {
|
||||
error!(pk=%pk, "charge_collateral_fees could not parse account: {err:?}");
|
||||
None
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let mut ix_to_send = Vec::new();
|
||||
let now_ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as u64;
|
||||
for (pk, account) in mango_accounts {
|
||||
let should_reset =
|
||||
collateral_fee_interval == 0 && account.fixed.last_collateral_fee_charge > 0;
|
||||
let should_charge = collateral_fee_interval > 0
|
||||
&& now_ts > account.fixed.last_collateral_fee_charge + collateral_fee_interval;
|
||||
if !(should_reset || should_charge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ixs = match client
|
||||
.token_charge_collateral_fees_instruction((&pk, &account))
|
||||
.await
|
||||
{
|
||||
Ok(ixs) => ixs,
|
||||
Err(err) => {
|
||||
error!(pk=%pk, "charge_collateral_fees could not build instruction: {err:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
ix_to_send.push(ixs);
|
||||
}
|
||||
|
||||
let txsigs = send_batched_log_errors_no_confirm(
|
||||
client.transaction_builder().await?,
|
||||
&client.client,
|
||||
&ix_to_send,
|
||||
max_cu_when_batching,
|
||||
)
|
||||
.await;
|
||||
info!("charge collateral fees: {:?}", txsigs);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try to batch the instructions into transactions and send them
|
||||
async fn send_batched_log_errors_no_confirm(
|
||||
mut tx_builder: TransactionBuilder,
|
||||
client: &mango_v4_client::Client,
|
||||
ixs_list: &[PreparedInstructions],
|
||||
max_cu: u32,
|
||||
) -> Vec<Signature> {
|
||||
let mut txsigs = Vec::new();
|
||||
|
||||
let mut current_batch = PreparedInstructions::new();
|
||||
for ixs in ixs_list {
|
||||
let previous_batch = current_batch.clone();
|
||||
current_batch.append(ixs.clone());
|
||||
|
||||
tx_builder.instructions = current_batch.clone().to_instructions();
|
||||
if tx_builder
|
||||
.transaction_size()
|
||||
.map(|ts| !ts.is_within_limit())
|
||||
.unwrap_or(true)
|
||||
|| current_batch.cu > max_cu
|
||||
{
|
||||
tx_builder.instructions = previous_batch.to_instructions();
|
||||
match tx_builder.send(client).await {
|
||||
Err(err) => error!("could not send transaction: {err:?}"),
|
||||
Ok(txsig) => txsigs.push(txsig),
|
||||
}
|
||||
|
||||
current_batch = ixs.clone();
|
||||
}
|
||||
}
|
||||
|
||||
if !current_batch.is_empty() {
|
||||
tx_builder.instructions = current_batch.to_instructions();
|
||||
match tx_builder.send(client).await {
|
||||
Err(err) => error!("could not send transaction: {err:?}"),
|
||||
Ok(txsig) => txsigs.push(txsig),
|
||||
}
|
||||
}
|
||||
|
||||
txsigs
|
||||
}
|
||||
|
|
|
@ -7,7 +7,10 @@ use std::time::Duration;
|
|||
use anchor_client::Cluster;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use mango_v4_client::{keypair_from_cli, Client, MangoClient, TransactionBuilderConfig};
|
||||
use mango_v4_client::{
|
||||
keypair_from_cli, priority_fees_cli, Client, FallbackOracleConfig, MangoClient,
|
||||
TransactionBuilderConfig,
|
||||
};
|
||||
use solana_sdk::commitment_config::CommitmentConfig;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use tokio::time;
|
||||
|
@ -58,12 +61,23 @@ struct Cli {
|
|||
#[clap(long, env, default_value_t = 120)]
|
||||
interval_check_new_listings_and_abort: u64,
|
||||
|
||||
#[clap(long, env, default_value_t = 300)]
|
||||
interval_charge_collateral_fees: u64,
|
||||
|
||||
#[clap(long, env, default_value_t = 10)]
|
||||
timeout: u64,
|
||||
|
||||
/// prioritize each transaction with this many microlamports/cu
|
||||
#[clap(long, env, default_value = "0")]
|
||||
prioritization_micro_lamports: u64,
|
||||
#[clap(flatten)]
|
||||
prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs,
|
||||
|
||||
/// url to the lite-rpc websocket, optional
|
||||
#[clap(long, env, default_value = "")]
|
||||
lite_rpc_url: String,
|
||||
|
||||
/// When batching multiple instructions into a transaction, don't exceed
|
||||
/// this compute unit limit.
|
||||
#[clap(long, env, default_value_t = 1_000_000)]
|
||||
max_cu_when_batching: u32,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
|
@ -85,6 +99,10 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
};
|
||||
let cli = Cli::parse_from(args);
|
||||
|
||||
let (prio_provider, prio_jobs) = cli
|
||||
.prioritization_fee_cli
|
||||
.make_prio_provider(cli.lite_rpc_url.clone())?;
|
||||
|
||||
let owner = Arc::new(keypair_from_cli(&cli.owner));
|
||||
|
||||
let rpc_url = cli.rpc_url;
|
||||
|
@ -98,19 +116,23 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
|
||||
let mango_client = Arc::new(
|
||||
MangoClient::new_for_existing_account(
|
||||
Client::new(
|
||||
cluster,
|
||||
commitment,
|
||||
owner.clone(),
|
||||
Some(Duration::from_secs(cli.timeout)),
|
||||
TransactionBuilderConfig {
|
||||
prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0)
|
||||
.then_some(cli.prioritization_micro_lamports),
|
||||
compute_budget_per_instruction: None,
|
||||
},
|
||||
),
|
||||
Client::builder()
|
||||
.cluster(cluster)
|
||||
.commitment(commitment)
|
||||
.fee_payer(Some(owner.clone()))
|
||||
.timeout(Duration::from_secs(cli.timeout))
|
||||
.transaction_builder_config(
|
||||
TransactionBuilderConfig::builder()
|
||||
.priority_fee_provider(prio_provider)
|
||||
.compute_budget_per_instruction(None)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.fallback_oracle_config(FallbackOracleConfig::Never)
|
||||
.build()
|
||||
.unwrap(),
|
||||
cli.mango_account,
|
||||
owner.clone(),
|
||||
owner,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
@ -139,12 +161,15 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
cli.interval_consume_events,
|
||||
cli.interval_update_funding,
|
||||
cli.interval_check_new_listings_and_abort,
|
||||
cli.interval_charge_collateral_fees,
|
||||
cli.max_cu_when_batching,
|
||||
prio_jobs,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Command::Taker { .. } => {
|
||||
let client = mango_client.clone();
|
||||
taker::runner(client, debugging_handle).await
|
||||
taker::runner(client, debugging_handle, prio_jobs).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,13 +10,15 @@ use mango_v4::{
|
|||
accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side},
|
||||
state::TokenIndex,
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::*;
|
||||
|
||||
use crate::MangoClient;
|
||||
|
||||
pub async fn runner(
|
||||
mango_client: Arc<MangoClient>,
|
||||
_debugging_handle: impl Future,
|
||||
debugging_handle: impl Future,
|
||||
extra_jobs: Vec<JoinHandle<()>>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
ensure_deposit(&mango_client).await?;
|
||||
ensure_oo(&mango_client).await?;
|
||||
|
@ -53,7 +55,9 @@ pub async fn runner(
|
|||
|
||||
futures::join!(
|
||||
futures::future::join_all(handles1),
|
||||
futures::future::join_all(handles2)
|
||||
futures::future::join_all(handles2),
|
||||
debugging_handle,
|
||||
futures::future::join_all(extra_jobs),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -48,3 +48,4 @@ tokio = { version = "1", features = ["full"] }
|
|||
tokio-stream = { version = "0.1.9"}
|
||||
tokio-tungstenite = "0.16.1"
|
||||
tracing = "0.1"
|
||||
regex = "1.9.5"
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
use crate::trigger_tcs;
|
||||
use anchor_lang::prelude::Pubkey;
|
||||
use clap::Parser;
|
||||
use mango_v4_client::{jupiter, priority_fees_cli};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap()]
|
||||
pub(crate) struct CliDotenv {
|
||||
// When --dotenv <file> is passed, read the specified dotenv file before parsing args
|
||||
#[clap(long)]
|
||||
pub(crate) dotenv: std::path::PathBuf,
|
||||
|
||||
pub(crate) remaining_args: Vec<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
// Prefer "--rebalance false" over "--no-rebalance" because it works
|
||||
// better with REBALANCE=false env values.
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum BoolArg {
|
||||
True,
|
||||
False,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum JupiterVersionArg {
|
||||
Mock,
|
||||
V6,
|
||||
}
|
||||
|
||||
impl From<JupiterVersionArg> for jupiter::Version {
|
||||
fn from(a: JupiterVersionArg) -> Self {
|
||||
match a {
|
||||
JupiterVersionArg::Mock => jupiter::Version::Mock,
|
||||
JupiterVersionArg::V6 => jupiter::Version::V6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum TcsMode {
|
||||
BorrowBuy,
|
||||
SwapSellIntoBuy,
|
||||
SwapCollateralIntoBuy,
|
||||
}
|
||||
|
||||
impl From<TcsMode> for trigger_tcs::Mode {
|
||||
fn from(a: TcsMode) -> Self {
|
||||
match a {
|
||||
TcsMode::BorrowBuy => trigger_tcs::Mode::BorrowBuyToken,
|
||||
TcsMode::SwapSellIntoBuy => trigger_tcs::Mode::SwapSellIntoBuy,
|
||||
TcsMode::SwapCollateralIntoBuy => trigger_tcs::Mode::SwapCollateralIntoBuy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cli_to_hashset<T: Eq + std::hash::Hash + From<u16>>(
|
||||
str_list: Option<Vec<u16>>,
|
||||
) -> HashSet<T> {
|
||||
return str_list
|
||||
.map(|v| v.iter().map(|x| T::from(*x)).collect::<HashSet<T>>())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap()]
|
||||
pub struct Cli {
|
||||
#[clap(short, long, env)]
|
||||
pub(crate) rpc_url: String,
|
||||
|
||||
#[clap(long, env, value_delimiter = ';')]
|
||||
pub(crate) override_send_transaction_url: Option<Vec<String>>,
|
||||
|
||||
#[clap(long, env)]
|
||||
pub(crate) liqor_mango_account: Pubkey,
|
||||
|
||||
#[clap(long, env)]
|
||||
pub(crate) liqor_owner: String,
|
||||
|
||||
#[clap(long, env, default_value = "1000")]
|
||||
pub(crate) check_interval_ms: u64,
|
||||
|
||||
#[clap(long, env, default_value = "300")]
|
||||
pub(crate) snapshot_interval_secs: u64,
|
||||
|
||||
// how often do we refresh token swap route/prices
|
||||
#[clap(long, env, default_value = "30")]
|
||||
pub(crate) token_swap_refresh_interval_secs: u64,
|
||||
|
||||
/// how many getMultipleAccounts requests to send in parallel
|
||||
#[clap(long, env, default_value = "10")]
|
||||
pub(crate) parallel_rpc_requests: usize,
|
||||
|
||||
/// typically 100 is the max number of accounts getMultipleAccounts will retrieve at once
|
||||
#[clap(long, env, default_value = "100")]
|
||||
pub(crate) get_multiple_accounts_count: usize,
|
||||
|
||||
/// liquidator health ratio should not fall below this value
|
||||
#[clap(long, env, default_value = "50")]
|
||||
pub(crate) min_health_ratio: f64,
|
||||
|
||||
/// if rebalancing is enabled
|
||||
///
|
||||
/// typically only disabled for tests where swaps are unavailable
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
pub(crate) rebalance: BoolArg,
|
||||
|
||||
/// max slippage to request on swaps to rebalance spot tokens
|
||||
#[clap(long, env, default_value = "100")]
|
||||
pub(crate) rebalance_slippage_bps: u64,
|
||||
|
||||
/// tokens to not rebalance (in addition to USDC=0); use a comma separated list of token index
|
||||
#[clap(long, env, value_parser, value_delimiter = ',')]
|
||||
pub(crate) rebalance_skip_tokens: Option<Vec<u16>>,
|
||||
|
||||
/// When closing borrows, the rebalancer can't close token positions exactly.
|
||||
/// Instead it purchases too much and then gets rid of the excess in a second step.
|
||||
/// If this is 0.05, then it'll swap borrow_value * (1 + 0.05) quote token into borrow token.
|
||||
#[clap(long, env, default_value = "0.05")]
|
||||
pub(crate) rebalance_borrow_settle_excess: f64,
|
||||
|
||||
#[clap(long, env, default_value = "30")]
|
||||
pub(crate) rebalance_refresh_timeout_secs: u64,
|
||||
|
||||
/// if taking tcs orders is enabled
|
||||
///
|
||||
/// typically only disabled for tests where swaps are unavailable
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
pub(crate) take_tcs: BoolArg,
|
||||
|
||||
/// profit margin at which to take tcs orders
|
||||
#[clap(long, env, default_value = "0.0005")]
|
||||
pub(crate) tcs_profit_fraction: f64,
|
||||
|
||||
/// control how tcs triggering provides buy tokens
|
||||
#[clap(long, env, value_enum, default_value = "swap-sell-into-buy")]
|
||||
pub(crate) tcs_mode: TcsMode,
|
||||
|
||||
/// largest tcs amount to trigger in one transaction, in dollar
|
||||
#[clap(long, env, default_value = "1000.0")]
|
||||
pub(crate) tcs_max_trigger_amount: f64,
|
||||
|
||||
/// Minimum fraction of max_buy to buy for success when triggering,
|
||||
/// useful in conjunction with jupiter swaps in same tx to avoid over-buying.
|
||||
///
|
||||
/// Can be set to 0 to allow executions of any size.
|
||||
#[clap(long, env, default_value = "0.7")]
|
||||
pub(crate) tcs_min_buy_fraction: f64,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs,
|
||||
|
||||
/// url to the lite-rpc websocket, optional
|
||||
#[clap(long, env, default_value = "")]
|
||||
pub(crate) lite_rpc_url: String,
|
||||
|
||||
/// compute limit requested for liquidation instructions
|
||||
#[clap(long, env, default_value = "250000")]
|
||||
pub(crate) compute_limit_for_liquidation: u32,
|
||||
|
||||
/// compute limit requested for tcs trigger instructions
|
||||
#[clap(long, env, default_value = "300000")]
|
||||
pub(crate) compute_limit_for_tcs: u32,
|
||||
|
||||
/// control which version of jupiter to use
|
||||
#[clap(long, env, value_enum, default_value = "v6")]
|
||||
pub(crate) jupiter_version: JupiterVersionArg,
|
||||
|
||||
/// override the url to jupiter v6
|
||||
#[clap(long, env, default_value = "https://quote-api.jup.ag/v6")]
|
||||
pub(crate) jupiter_v6_url: String,
|
||||
|
||||
/// provide a jupiter token, currently only for jup v6
|
||||
#[clap(long, env, default_value = "")]
|
||||
pub(crate) jupiter_token: String,
|
||||
|
||||
/// size of the swap to quote via jupiter to get slippage info, in dollar
|
||||
/// should be larger than tcs_max_trigger_amount
|
||||
#[clap(long, env, default_value = "1000.0")]
|
||||
pub(crate) jupiter_swap_info_amount: f64,
|
||||
|
||||
/// report liquidator's existence and pubkey
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
pub(crate) telemetry: BoolArg,
|
||||
|
||||
/// liquidation refresh timeout in secs
|
||||
#[clap(long, env, default_value = "30")]
|
||||
pub(crate) liquidation_refresh_timeout_secs: u8,
|
||||
|
||||
/// tokens to exclude for liquidation/tcs (never liquidate any pair where base or quote is in this list)
|
||||
#[clap(long, env, value_parser, value_delimiter = ' ')]
|
||||
pub(crate) forbidden_tokens: Option<Vec<u16>>,
|
||||
|
||||
/// tokens to allow for liquidation/tcs (only liquidate a pair if base or quote is in this list)
|
||||
/// when empty, allows all pairs
|
||||
#[clap(long, env, value_parser, value_delimiter = ' ')]
|
||||
pub(crate) only_allow_tokens: Option<Vec<u16>>,
|
||||
|
||||
/// perp market to exclude for liquidation
|
||||
#[clap(long, env, value_parser, value_delimiter = ' ')]
|
||||
pub(crate) liquidation_forbidden_perp_markets: Option<Vec<u16>>,
|
||||
|
||||
/// perp market to allow for liquidation (only liquidate if is in this list)
|
||||
/// when empty, allows all pairs
|
||||
#[clap(long, env, value_parser, value_delimiter = ' ')]
|
||||
pub(crate) liquidation_only_allow_perp_markets: Option<Vec<u16>>,
|
||||
|
||||
/// how long should it wait before logging an oracle error again (for the same token)
|
||||
#[clap(long, env, default_value = "30")]
|
||||
pub(crate) skip_oracle_error_in_logs_duration_secs: u64,
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
use std::cmp::Reverse;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use itertools::Itertools;
|
||||
use mango_v4::health::{HealthCache, HealthType};
|
||||
use mango_v4::state::{MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX};
|
||||
use mango_v4_client::{chain_data, health_cache, MangoClient};
|
||||
use mango_v4_client::{chain_data, MangoClient, PreparedInstructions};
|
||||
use solana_sdk::signature::Signature;
|
||||
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
|
@ -19,6 +20,16 @@ pub struct Config {
|
|||
pub min_health_ratio: f64,
|
||||
pub refresh_timeout: Duration,
|
||||
pub compute_limit_for_liq_ix: u32,
|
||||
|
||||
pub only_allowed_tokens: HashSet<TokenIndex>,
|
||||
pub forbidden_tokens: HashSet<TokenIndex>,
|
||||
|
||||
pub only_allowed_perp_markets: HashSet<PerpMarketIndex>,
|
||||
pub forbidden_perp_markets: HashSet<PerpMarketIndex>,
|
||||
|
||||
/// If we cram multiple ix into a transaction, don't exceed this level
|
||||
/// of expected-cu.
|
||||
pub max_cu_per_transaction: u32,
|
||||
}
|
||||
|
||||
struct LiquidateHelper<'a> {
|
||||
|
@ -29,8 +40,6 @@ struct LiquidateHelper<'a> {
|
|||
health_cache: &'a HealthCache,
|
||||
maint_health: I80F48,
|
||||
liqor_min_health_ratio: I80F48,
|
||||
allowed_asset_tokens: HashSet<Pubkey>,
|
||||
allowed_liab_tokens: HashSet<Pubkey>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
|
@ -46,7 +55,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok((*orders, *open_orders))
|
||||
})
|
||||
.try_collect();
|
||||
let serum_force_cancels = serum_oos?
|
||||
let mut serum_force_cancels = serum_oos?
|
||||
.into_iter()
|
||||
.filter_map(|(orders, open_orders)| {
|
||||
let can_force_cancel = open_orders.native_coin_total > 0
|
||||
|
@ -62,18 +71,42 @@ impl<'a> LiquidateHelper<'a> {
|
|||
if serum_force_cancels.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
// Cancel all orders on a random serum market
|
||||
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
||||
let txsig = self
|
||||
.client
|
||||
.serum3_liq_force_cancel_orders(
|
||||
(self.pubkey, self.liqee),
|
||||
serum_orders.market_index,
|
||||
&serum_orders.open_orders,
|
||||
)
|
||||
.await?;
|
||||
serum_force_cancels.shuffle(&mut rand::thread_rng());
|
||||
|
||||
let mut ixs = PreparedInstructions::new();
|
||||
let mut cancelled_markets = vec![];
|
||||
let mut tx_builder = self.client.transaction_builder().await?;
|
||||
|
||||
for force_cancel in serum_force_cancels {
|
||||
let mut new_ixs = ixs.clone();
|
||||
new_ixs.append(
|
||||
self.client
|
||||
.serum3_liq_force_cancel_orders_instruction(
|
||||
(self.pubkey, self.liqee),
|
||||
force_cancel.market_index,
|
||||
&force_cancel.open_orders,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
let exceeds_cu_limit = new_ixs.cu > self.config.max_cu_per_transaction;
|
||||
let exceeds_size_limit = {
|
||||
tx_builder.instructions = new_ixs.clone().to_instructions();
|
||||
!tx_builder.transaction_size()?.is_within_limit()
|
||||
};
|
||||
if exceeds_cu_limit || exceeds_size_limit {
|
||||
break;
|
||||
}
|
||||
|
||||
ixs = new_ixs;
|
||||
cancelled_markets.push(force_cancel.market_index);
|
||||
}
|
||||
|
||||
tx_builder.instructions = ixs.to_instructions();
|
||||
|
||||
let txsig = tx_builder.send_and_confirm(&self.client.client).await?;
|
||||
info!(
|
||||
market_index = serum_orders.market_index,
|
||||
market_indexes = ?cancelled_markets,
|
||||
%txsig,
|
||||
"Force cancelled serum orders",
|
||||
);
|
||||
|
@ -108,6 +141,25 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let all_perp_base_positions: anyhow::Result<
|
||||
Vec<Option<(PerpMarketIndex, i64, I80F48, I80F48)>>,
|
||||
> = stream::iter(self.liqee.active_perp_positions())
|
||||
.filter(|pp| async {
|
||||
if self
|
||||
.config
|
||||
.forbidden_perp_markets
|
||||
.contains(&pp.market_index)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if !self.config.only_allowed_perp_markets.is_empty()
|
||||
&& !self
|
||||
.config
|
||||
.only_allowed_perp_markets
|
||||
.contains(&pp.market_index)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.then(|pp| async {
|
||||
let base_lots = pp.base_position_lots();
|
||||
if (base_lots == 0 && pp.quote_position_native() <= 0) || pp.has_open_taker_fills()
|
||||
|
@ -155,10 +207,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.await
|
||||
.context("getting liquidator account")?;
|
||||
liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?;
|
||||
let mut health_cache =
|
||||
health_cache::new(&self.client.context, self.account_fetcher, &liqor)
|
||||
.await
|
||||
.context("health cache")?;
|
||||
let mut health_cache = self.client.health_cache(&liqor).await.expect("always ok");
|
||||
let quote_bank = self
|
||||
.client
|
||||
.first_bank(QUOTE_TOKEN_INDEX)
|
||||
|
@ -328,6 +377,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.health_cache
|
||||
.token_infos
|
||||
.iter()
|
||||
.filter(|p| !self.config.forbidden_tokens.contains(&p.token_index))
|
||||
.zip(
|
||||
self.health_cache
|
||||
.effective_token_balances(HealthType::LiquidationEnd)
|
||||
|
@ -345,34 +395,17 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.filter_map(|(ti, effective)| {
|
||||
// check constraints for liquidatable assets, see also has_possible_spot_liquidations()
|
||||
let tokens = ti.balance_spot.min(effective.spot_and_perp);
|
||||
let is_valid_asset = tokens >= 1;
|
||||
let is_valid_asset = tokens >= 1 && ti.allow_asset_liquidation;
|
||||
let quote_value = tokens * ti.prices.oracle;
|
||||
// prefer to liquidate tokens with asset weight that have >$1 liquidatable
|
||||
let is_preferred =
|
||||
ti.init_asset_weight > 0 && quote_value > I80F48::from(1_000_000);
|
||||
ti.maint_asset_weight > 0 && quote_value > I80F48::from(1_000_000);
|
||||
is_valid_asset.then_some((ti.token_index, is_preferred, quote_value))
|
||||
})
|
||||
.collect_vec();
|
||||
// sort such that preferred tokens are at the end, and the one with the larget quote value is
|
||||
// at the very end
|
||||
potential_assets.sort_by_key(|(_, is_preferred, amount)| (*is_preferred, *amount));
|
||||
|
||||
// filter only allowed assets
|
||||
let potential_allowed_assets = potential_assets.iter().filter_map(|(ti, _, _)| {
|
||||
let is_allowed = self
|
||||
.allowed_asset_tokens
|
||||
.contains(&self.client.context.token(*ti).mint);
|
||||
is_allowed.then_some(*ti)
|
||||
});
|
||||
|
||||
let asset_token_index = match potential_allowed_assets.last() {
|
||||
Some(token_index) => token_index,
|
||||
None => anyhow::bail!(
|
||||
"mango account {}, has no allowed asset tokens that are liquidatable: {:?}",
|
||||
self.pubkey,
|
||||
potential_assets,
|
||||
),
|
||||
};
|
||||
// sort such that preferred tokens are at the start, and the one with the larget quote value is
|
||||
// at 0
|
||||
potential_assets.sort_by_key(|(_, is_preferred, amount)| Reverse((*is_preferred, *amount)));
|
||||
|
||||
//
|
||||
// find a good liab, same as for assets
|
||||
|
@ -385,29 +418,69 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let tokens = (-ti.balance_spot).min(-effective.spot_and_perp);
|
||||
let is_valid_liab = tokens > 0;
|
||||
let quote_value = tokens * ti.prices.oracle;
|
||||
is_valid_liab.then_some((ti.token_index, quote_value))
|
||||
is_valid_liab.then_some((ti.token_index, false, quote_value))
|
||||
})
|
||||
.collect_vec();
|
||||
// largest liquidatable liability at the end
|
||||
potential_liabs.sort_by_key(|(_, amount)| *amount);
|
||||
// largest liquidatable liability at the start
|
||||
potential_liabs.sort_by_key(|(_, is_preferred, amount)| Reverse((*is_preferred, *amount)));
|
||||
|
||||
// filter only allowed liabs
|
||||
let potential_allowed_liabs = potential_liabs.iter().filter_map(|(ti, _)| {
|
||||
let is_allowed = self
|
||||
.allowed_liab_tokens
|
||||
.contains(&self.client.context.token(*ti).mint);
|
||||
is_allowed.then_some(*ti)
|
||||
});
|
||||
//
|
||||
// Find a pair
|
||||
//
|
||||
|
||||
let liab_token_index = match potential_allowed_liabs.last() {
|
||||
Some(token_index) => token_index,
|
||||
None => anyhow::bail!(
|
||||
"mango account {}, has no liab tokens that are liquidatable: {:?}",
|
||||
fn find_best_token(
|
||||
lh: &LiquidateHelper,
|
||||
token_list: &Vec<(TokenIndex, bool, I80F48)>,
|
||||
) -> (Option<TokenIndex>, Option<TokenIndex>) {
|
||||
let mut best_whitelisted = None;
|
||||
let mut best = None;
|
||||
|
||||
let allowed_token_list = token_list
|
||||
.iter()
|
||||
.filter_map(|(ti, _, _)| (!lh.config.forbidden_tokens.contains(ti)).then_some(ti));
|
||||
|
||||
for ti in allowed_token_list {
|
||||
let whitelisted = lh.config.only_allowed_tokens.is_empty()
|
||||
|| lh.config.only_allowed_tokens.contains(ti);
|
||||
if best.is_none() {
|
||||
best = Some(*ti);
|
||||
}
|
||||
|
||||
if best_whitelisted.is_none() && whitelisted {
|
||||
best_whitelisted = Some(*ti);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (best, best_whitelisted);
|
||||
}
|
||||
|
||||
let (best_asset, best_whitelisted_asset) = find_best_token(self, &potential_assets);
|
||||
let (best_liab, best_whitelisted_liab) = find_best_token(self, &potential_liabs);
|
||||
|
||||
let best_pair_opt = [
|
||||
(best_whitelisted_asset, best_liab),
|
||||
(best_asset, best_whitelisted_liab),
|
||||
]
|
||||
.iter()
|
||||
.filter_map(|(a, l)| (a.is_some() && l.is_some()).then_some((a.unwrap(), l.unwrap())))
|
||||
.next();
|
||||
|
||||
if best_pair_opt.is_none() {
|
||||
anyhow::bail!(
|
||||
"mango account {}, has no allowed asset/liab tokens pair that are liquidatable: assets={:?}; liabs={:?}",
|
||||
self.pubkey,
|
||||
potential_assets,
|
||||
potential_liabs,
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
let (asset_token_index, liab_token_index) = best_pair_opt.unwrap();
|
||||
|
||||
//
|
||||
// Compute max transfer size
|
||||
//
|
||||
|
||||
let max_liab_transfer = self
|
||||
.max_token_liab_transfer(liab_token_index, asset_token_index)
|
||||
.await
|
||||
|
@ -459,9 +532,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.iter()
|
||||
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
||||
liab_usdc_equivalent.is_negative()
|
||||
&& self
|
||||
.allowed_liab_tokens
|
||||
.contains(&self.client.context.token(*liab_token_index).mint)
|
||||
&& !self.config.forbidden_tokens.contains(liab_token_index)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
|
@ -589,7 +660,8 @@ pub async fn maybe_liquidate_account(
|
|||
let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio);
|
||||
|
||||
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
||||
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
|
||||
let health_cache = mango_client
|
||||
.health_cache(&account)
|
||||
.await
|
||||
.context("creating health cache 1")?;
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
|
@ -607,7 +679,8 @@ pub async fn maybe_liquidate_account(
|
|||
// This is -- unfortunately -- needed because the websocket streams seem to not
|
||||
// be great at providing timely updates to the account data.
|
||||
let account = account_fetcher.fetch_fresh_mango_account(pubkey).await?;
|
||||
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
|
||||
let health_cache = mango_client
|
||||
.health_cache(&account)
|
||||
.await
|
||||
.context("creating health cache 2")?;
|
||||
if !health_cache.is_liquidatable() {
|
||||
|
@ -616,8 +689,6 @@ pub async fn maybe_liquidate_account(
|
|||
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
|
||||
let all_token_mints = HashSet::from_iter(mango_client.context.tokens.values().map(|c| c.mint));
|
||||
|
||||
// try liquidating
|
||||
let maybe_txsig = LiquidateHelper {
|
||||
client: mango_client,
|
||||
|
@ -627,8 +698,6 @@ pub async fn maybe_liquidate_account(
|
|||
health_cache: &health_cache,
|
||||
maint_health,
|
||||
liqor_min_health_ratio,
|
||||
allowed_asset_tokens: all_token_mints.clone(),
|
||||
allowed_liab_tokens: all_token_mints,
|
||||
config: config.clone(),
|
||||
}
|
||||
.send_liq_tx()
|
||||
|
|
|
@ -9,7 +9,7 @@ use clap::Parser;
|
|||
use mango_v4::state::{PerpMarketIndex, TokenIndex};
|
||||
use mango_v4_client::AsyncChannelSendUnlessFull;
|
||||
use mango_v4_client::{
|
||||
account_update_stream, chain_data, error_tracking::ErrorTracking, jupiter, keypair_from_cli,
|
||||
account_update_stream, chain_data, error_tracking::ErrorTracking, keypair_from_cli,
|
||||
snapshot_source, websocket_source, Client, MangoClient, MangoClientError, MangoGroupContext,
|
||||
TransactionBuilderConfig,
|
||||
};
|
||||
|
@ -20,14 +20,17 @@ use solana_sdk::pubkey::Pubkey;
|
|||
use solana_sdk::signer::Signer;
|
||||
use tracing::*;
|
||||
|
||||
pub mod cli_args;
|
||||
pub mod liquidate;
|
||||
pub mod metrics;
|
||||
pub mod rebalance;
|
||||
pub mod telemetry;
|
||||
pub mod token_swap_info;
|
||||
pub mod trigger_tcs;
|
||||
mod unwrappable_oracle_error;
|
||||
pub mod util;
|
||||
|
||||
use crate::unwrappable_oracle_error::UnwrappableOracleError;
|
||||
use crate::util::{is_mango_account, is_mint_info, is_perp_market};
|
||||
|
||||
// jemalloc seems to be better at keeping the memory footprint reasonable over
|
||||
|
@ -35,149 +38,6 @@ use crate::util::{is_mango_account, is_mint_info, is_perp_market};
|
|||
#[global_allocator]
|
||||
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap()]
|
||||
struct CliDotenv {
|
||||
// When --dotenv <file> is passed, read the specified dotenv file before parsing args
|
||||
#[clap(long)]
|
||||
dotenv: std::path::PathBuf,
|
||||
|
||||
remaining_args: Vec<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
// Prefer "--rebalance false" over "--no-rebalance" because it works
|
||||
// better with REBALANCE=false env values.
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum BoolArg {
|
||||
True,
|
||||
False,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum JupiterVersionArg {
|
||||
Mock,
|
||||
V4,
|
||||
V6,
|
||||
}
|
||||
|
||||
impl From<JupiterVersionArg> for jupiter::Version {
|
||||
fn from(a: JupiterVersionArg) -> Self {
|
||||
match a {
|
||||
JupiterVersionArg::Mock => jupiter::Version::Mock,
|
||||
JupiterVersionArg::V4 => jupiter::Version::V4,
|
||||
JupiterVersionArg::V6 => jupiter::Version::V6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum TcsMode {
|
||||
BorrowBuy,
|
||||
SwapSellIntoBuy,
|
||||
SwapCollateralIntoBuy,
|
||||
}
|
||||
|
||||
impl From<TcsMode> for trigger_tcs::Mode {
|
||||
fn from(a: TcsMode) -> Self {
|
||||
match a {
|
||||
TcsMode::BorrowBuy => trigger_tcs::Mode::BorrowBuyToken,
|
||||
TcsMode::SwapSellIntoBuy => trigger_tcs::Mode::SwapSellIntoBuy,
|
||||
TcsMode::SwapCollateralIntoBuy => trigger_tcs::Mode::SwapCollateralIntoBuy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap()]
|
||||
struct Cli {
|
||||
#[clap(short, long, env)]
|
||||
rpc_url: String,
|
||||
|
||||
#[clap(long, env)]
|
||||
liqor_mango_account: Pubkey,
|
||||
|
||||
#[clap(long, env)]
|
||||
liqor_owner: String,
|
||||
|
||||
#[clap(long, env, default_value = "1000")]
|
||||
check_interval_ms: u64,
|
||||
|
||||
#[clap(long, env, default_value = "300")]
|
||||
snapshot_interval_secs: u64,
|
||||
|
||||
/// how many getMultipleAccounts requests to send in parallel
|
||||
#[clap(long, env, default_value = "10")]
|
||||
parallel_rpc_requests: usize,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// if rebalancing is enabled
|
||||
///
|
||||
/// typically only disabled for tests where swaps are unavailable
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
rebalance: BoolArg,
|
||||
|
||||
/// max slippage to request on swaps to rebalance spot tokens
|
||||
#[clap(long, env, default_value = "100")]
|
||||
rebalance_slippage_bps: u64,
|
||||
|
||||
/// tokens to not rebalance (in addition to USDC); use a comma separated list of names
|
||||
#[clap(long, env, default_value = "")]
|
||||
rebalance_skip_tokens: String,
|
||||
|
||||
/// if taking tcs orders is enabled
|
||||
///
|
||||
/// typically only disabled for tests where swaps are unavailable
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
take_tcs: BoolArg,
|
||||
|
||||
/// profit margin at which to take tcs orders
|
||||
#[clap(long, env, default_value = "0.0005")]
|
||||
tcs_profit_fraction: f64,
|
||||
|
||||
/// control how tcs triggering provides buy tokens
|
||||
#[clap(long, env, value_enum, default_value = "swap-sell-into-buy")]
|
||||
tcs_mode: TcsMode,
|
||||
|
||||
/// prioritize each transaction with this many microlamports/cu
|
||||
#[clap(long, env, default_value = "0")]
|
||||
prioritization_micro_lamports: u64,
|
||||
|
||||
/// compute limit requested for liquidation instructions
|
||||
#[clap(long, env, default_value = "250000")]
|
||||
compute_limit_for_liquidation: u32,
|
||||
|
||||
/// compute limit requested for tcs trigger instructions
|
||||
#[clap(long, env, default_value = "300000")]
|
||||
compute_limit_for_tcs: u32,
|
||||
|
||||
/// control which version of jupiter to use
|
||||
#[clap(long, env, value_enum, default_value = "v6")]
|
||||
jupiter_version: JupiterVersionArg,
|
||||
|
||||
/// override the url to jupiter v4
|
||||
#[clap(long, env, default_value = "https://quote-api.jup.ag/v4")]
|
||||
jupiter_v4_url: String,
|
||||
|
||||
/// override the url to jupiter v6
|
||||
#[clap(long, env, default_value = "https://quote-api.jup.ag/v6")]
|
||||
jupiter_v6_url: String,
|
||||
|
||||
/// provide a jupiter token, currently only for jup v6
|
||||
#[clap(long, env, default_value = "")]
|
||||
jupiter_token: String,
|
||||
|
||||
/// report liquidator's existence and pubkey
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
telemetry: BoolArg,
|
||||
}
|
||||
|
||||
pub fn encode_address(addr: &Pubkey) -> String {
|
||||
bs58::encode(&addr.to_bytes()).into_string()
|
||||
}
|
||||
|
@ -186,20 +46,31 @@ pub fn encode_address(addr: &Pubkey) -> String {
|
|||
async fn main() -> anyhow::Result<()> {
|
||||
mango_v4_client::tracing_subscriber_init();
|
||||
|
||||
let args = if let Ok(cli_dotenv) = CliDotenv::try_parse() {
|
||||
let args: Vec<std::ffi::OsString> = if let Ok(cli_dotenv) = CliDotenv::try_parse() {
|
||||
dotenv::from_path(cli_dotenv.dotenv)?;
|
||||
cli_dotenv.remaining_args
|
||||
std::env::args_os()
|
||||
.take(1)
|
||||
.chain(cli_dotenv.remaining_args.into_iter())
|
||||
.collect()
|
||||
} else {
|
||||
dotenv::dotenv().ok();
|
||||
std::env::args_os().collect()
|
||||
};
|
||||
let cli = Cli::parse_from(args);
|
||||
|
||||
let liqor_owner = Arc::new(keypair_from_cli(&cli.liqor_owner));
|
||||
//
|
||||
// Priority fee setup
|
||||
//
|
||||
let (prio_provider, prio_jobs) = cli
|
||||
.prioritization_fee_cli
|
||||
.make_prio_provider(cli.lite_rpc_url.clone())?;
|
||||
|
||||
//
|
||||
// Client setup
|
||||
//
|
||||
let liqor_owner = Arc::new(keypair_from_cli(&cli.liqor_owner));
|
||||
let rpc_url = cli.rpc_url;
|
||||
let ws_url = rpc_url.replace("https", "wss");
|
||||
|
||||
let rpc_timeout = Duration::from_secs(10);
|
||||
let cluster = Cluster::Custom(rpc_url.clone(), ws_url.clone());
|
||||
let commitment = CommitmentConfig::processed();
|
||||
|
@ -207,16 +78,18 @@ async fn main() -> anyhow::Result<()> {
|
|||
.cluster(cluster.clone())
|
||||
.commitment(commitment)
|
||||
.fee_payer(Some(liqor_owner.clone()))
|
||||
.timeout(Some(rpc_timeout))
|
||||
.jupiter_v4_url(cli.jupiter_v4_url)
|
||||
.timeout(rpc_timeout)
|
||||
.jupiter_v6_url(cli.jupiter_v6_url)
|
||||
.jupiter_token(cli.jupiter_token)
|
||||
.transaction_builder_config(TransactionBuilderConfig {
|
||||
prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0)
|
||||
.then_some(cli.prioritization_micro_lamports),
|
||||
// Liquidation and tcs triggers set their own budgets, this is a default for other tx
|
||||
compute_budget_per_instruction: Some(250_000),
|
||||
})
|
||||
.transaction_builder_config(
|
||||
TransactionBuilderConfig::builder()
|
||||
.priority_fee_provider(prio_provider)
|
||||
// Liquidation and tcs triggers set their own budgets, this is a default for other tx
|
||||
.compute_budget_per_instruction(Some(250_000))
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.override_send_transaction_urls(cli.override_send_transaction_url)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
@ -225,7 +98,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
// Reading accounts from chain_data
|
||||
let account_fetcher = Arc::new(chain_data::AccountFetcher {
|
||||
chain_data: chain_data.clone(),
|
||||
rpc: client.rpc_async(),
|
||||
rpc: client.new_rpc_async(),
|
||||
});
|
||||
|
||||
let mango_account = account_fetcher
|
||||
|
@ -238,7 +111,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
warn!("rebalancing on delegated accounts will be unable to free token positions reliably, withdraw dust manually");
|
||||
}
|
||||
|
||||
let group_context = MangoGroupContext::new_from_rpc(&client.rpc_async(), mango_group).await?;
|
||||
let group_context = MangoGroupContext::new_from_rpc(client.rpc_async(), mango_group).await?;
|
||||
|
||||
let mango_oracles = group_context
|
||||
.tokens
|
||||
|
@ -319,8 +192,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
};
|
||||
|
||||
let token_swap_info_config = token_swap_info::Config {
|
||||
quote_index: 0, // USDC
|
||||
quote_amount: 1_000_000_000, // TODO: config, $1000, should be >= tcs_config.max_trigger_quote_amount
|
||||
quote_index: 0, // USDC
|
||||
quote_amount: (cli.jupiter_swap_info_amount * 1e6) as u64,
|
||||
jupiter_version: cli.jupiter_version.into(),
|
||||
};
|
||||
|
||||
|
@ -332,24 +205,33 @@ async fn main() -> anyhow::Result<()> {
|
|||
let liq_config = liquidate::Config {
|
||||
min_health_ratio: cli.min_health_ratio,
|
||||
compute_limit_for_liq_ix: cli.compute_limit_for_liquidation,
|
||||
// TODO: config
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
max_cu_per_transaction: 1_000_000,
|
||||
refresh_timeout: Duration::from_secs(cli.liquidation_refresh_timeout_secs as u64),
|
||||
only_allowed_tokens: cli_args::cli_to_hashset::<TokenIndex>(cli.only_allow_tokens),
|
||||
forbidden_tokens: cli_args::cli_to_hashset::<TokenIndex>(cli.forbidden_tokens),
|
||||
only_allowed_perp_markets: cli_args::cli_to_hashset::<PerpMarketIndex>(
|
||||
cli.liquidation_only_allow_perp_markets,
|
||||
),
|
||||
forbidden_perp_markets: cli_args::cli_to_hashset::<PerpMarketIndex>(
|
||||
cli.liquidation_forbidden_perp_markets,
|
||||
),
|
||||
};
|
||||
|
||||
let tcs_config = trigger_tcs::Config {
|
||||
min_health_ratio: cli.min_health_ratio,
|
||||
max_trigger_quote_amount: 1_000_000_000, // TODO: config, $1000
|
||||
max_trigger_quote_amount: (cli.tcs_max_trigger_amount * 1e6) as u64,
|
||||
compute_limit_for_trigger: cli.compute_limit_for_tcs,
|
||||
profit_fraction: cli.tcs_profit_fraction,
|
||||
collateral_token_index: 0, // USDC
|
||||
// TODO: config
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
|
||||
jupiter_version: cli.jupiter_version.into(),
|
||||
jupiter_slippage_bps: cli.rebalance_slippage_bps,
|
||||
|
||||
mode: cli.tcs_mode.into(),
|
||||
min_buy_fraction: 0.7,
|
||||
min_buy_fraction: cli.tcs_min_buy_fraction,
|
||||
|
||||
only_allowed_tokens: liq_config.only_allowed_tokens.clone(),
|
||||
forbidden_tokens: liq_config.forbidden_tokens.clone(),
|
||||
};
|
||||
|
||||
let mut rebalance_interval = tokio::time::interval(Duration::from_secs(30));
|
||||
|
@ -357,16 +239,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
let rebalance_config = rebalance::Config {
|
||||
enabled: cli.rebalance == BoolArg::True,
|
||||
slippage_bps: cli.rebalance_slippage_bps,
|
||||
// TODO: config
|
||||
borrow_settle_excess: 1.05,
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
borrow_settle_excess: (1f64 + cli.rebalance_borrow_settle_excess).max(1f64),
|
||||
refresh_timeout: Duration::from_secs(cli.rebalance_refresh_timeout_secs),
|
||||
jupiter_version: cli.jupiter_version.into(),
|
||||
skip_tokens: cli
|
||||
.rebalance_skip_tokens
|
||||
.split(',')
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(|name| mango_client.context.token_by_name(name).token_index)
|
||||
.collect(),
|
||||
skip_tokens: cli.rebalance_skip_tokens.unwrap_or(Vec::new()),
|
||||
allow_withdraws: signer_is_owner,
|
||||
};
|
||||
|
||||
|
@ -388,6 +264,12 @@ async fn main() -> anyhow::Result<()> {
|
|||
.skip_threshold_for_type(LiqErrorType::Liq, 5)
|
||||
.skip_duration(Duration::from_secs(120))
|
||||
.build()?,
|
||||
oracle_errors: ErrorTracking::builder()
|
||||
.skip_threshold(1)
|
||||
.skip_duration(Duration::from_secs(
|
||||
cli.skip_oracle_error_in_logs_duration_secs,
|
||||
))
|
||||
.build()?,
|
||||
});
|
||||
|
||||
info!("main loop");
|
||||
|
@ -501,6 +383,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
};
|
||||
|
||||
liquidation.errors.update();
|
||||
liquidation.oracle_errors.update();
|
||||
|
||||
let liquidated = liquidation
|
||||
.maybe_liquidate_one(account_addresses.iter())
|
||||
|
@ -508,16 +391,13 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
let mut took_tcs = false;
|
||||
if !liquidated && cli.take_tcs == BoolArg::True {
|
||||
took_tcs = match liquidation
|
||||
took_tcs = liquidation
|
||||
.maybe_take_token_conditional_swap(account_addresses.iter())
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
.unwrap_or_else(|err| {
|
||||
error!("error during maybe_take_token_conditional_swap: {err}");
|
||||
false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if liquidated || took_tcs {
|
||||
|
@ -528,14 +408,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
});
|
||||
|
||||
let token_swap_info_job = tokio::spawn({
|
||||
// TODO: configurable interval
|
||||
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(60));
|
||||
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(
|
||||
cli.token_swap_refresh_interval_secs,
|
||||
));
|
||||
let mut startup_wait = mango_v4_client::delay_interval(Duration::from_secs(1));
|
||||
let shared_state = shared_state.clone();
|
||||
async move {
|
||||
loop {
|
||||
startup_wait.tick().await;
|
||||
if !shared_state.read().unwrap().one_snapshot_done {
|
||||
startup_wait.tick().await;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -570,6 +451,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
));
|
||||
}
|
||||
|
||||
use cli_args::{BoolArg, Cli, CliDotenv};
|
||||
use futures::StreamExt;
|
||||
let mut jobs: futures::stream::FuturesUnordered<_> = vec![
|
||||
data_job,
|
||||
|
@ -579,6 +461,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
check_changes_for_abort_job,
|
||||
]
|
||||
.into_iter()
|
||||
.chain(prio_jobs.into_iter())
|
||||
.collect();
|
||||
jobs.next().await;
|
||||
|
||||
|
@ -625,6 +508,7 @@ struct LiquidationState {
|
|||
trigger_tcs_config: trigger_tcs::Config,
|
||||
|
||||
errors: ErrorTracking<Pubkey, LiqErrorType>,
|
||||
oracle_errors: ErrorTracking<TokenIndex, LiqErrorType>,
|
||||
}
|
||||
|
||||
impl LiquidationState {
|
||||
|
@ -678,6 +562,25 @@ impl LiquidationState {
|
|||
.await;
|
||||
|
||||
if let Err(err) = result.as_ref() {
|
||||
if let Some((ti, ti_name)) = err.try_unwrap_oracle_error() {
|
||||
if self
|
||||
.oracle_errors
|
||||
.had_too_many_errors(LiqErrorType::Liq, &ti, Instant::now())
|
||||
.is_none()
|
||||
{
|
||||
warn!(
|
||||
"{:?} recording oracle error for token {} {}",
|
||||
chrono::offset::Utc::now(),
|
||||
ti_name,
|
||||
ti
|
||||
);
|
||||
}
|
||||
|
||||
self.oracle_errors
|
||||
.record(LiqErrorType::Liq, &ti, err.to_string());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Keep track of pubkeys that had errors
|
||||
error_tracking.record(LiqErrorType::Liq, pubkey, err.to_string());
|
||||
|
||||
|
|
|
@ -151,18 +151,7 @@ impl Rebalancer {
|
|||
let direct_sol_route_job =
|
||||
self.jupiter_quote(sol_mint, output_mint, in_amount_sol, true, jupiter_version);
|
||||
|
||||
let mut jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
|
||||
|
||||
// for v6, add a v4 fallback
|
||||
if self.config.jupiter_version == jupiter::Version::V6 {
|
||||
jobs.push(self.jupiter_quote(
|
||||
quote_mint,
|
||||
output_mint,
|
||||
in_amount_quote,
|
||||
false,
|
||||
jupiter::Version::V4,
|
||||
));
|
||||
}
|
||||
let jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
|
||||
|
||||
let mut results = futures::future::join_all(jobs).await;
|
||||
let full_route = results.remove(0)?;
|
||||
|
@ -211,18 +200,7 @@ impl Rebalancer {
|
|||
let direct_sol_route_job =
|
||||
self.jupiter_quote(input_mint, sol_mint, in_amount, true, jupiter_version);
|
||||
|
||||
let mut jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
|
||||
|
||||
// for v6, add a v4 fallback
|
||||
if self.config.jupiter_version == jupiter::Version::V6 {
|
||||
jobs.push(self.jupiter_quote(
|
||||
input_mint,
|
||||
quote_mint,
|
||||
in_amount,
|
||||
false,
|
||||
jupiter::Version::V4,
|
||||
));
|
||||
}
|
||||
let jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
|
||||
|
||||
let mut results = futures::future::join_all(jobs).await;
|
||||
let full_route = results.remove(0)?;
|
||||
|
@ -253,7 +231,7 @@ impl Rebalancer {
|
|||
.prepare_swap_transaction(full)
|
||||
.await?;
|
||||
let tx_size = builder.transaction_size()?;
|
||||
if tx_size.is_ok() {
|
||||
if tx_size.is_within_limit() {
|
||||
return Ok((builder, full.clone()));
|
||||
}
|
||||
trace!(
|
||||
|
@ -520,6 +498,7 @@ impl Rebalancer {
|
|||
};
|
||||
let counters = perp_pnl::fetch_top(
|
||||
&self.mango_client.context,
|
||||
&self.mango_client.client.config().fallback_oracle_config,
|
||||
self.account_fetcher.as_ref(),
|
||||
perp_position.market_index,
|
||||
direction,
|
||||
|
|
|
@ -11,7 +11,10 @@ use mango_v4_client::MangoClient;
|
|||
|
||||
pub struct Config {
|
||||
pub quote_index: TokenIndex,
|
||||
|
||||
/// Size in quote_index-token native tokens to quote.
|
||||
pub quote_amount: u64,
|
||||
|
||||
pub jupiter_version: jupiter::Version,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use std::collections::HashSet;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
pin::Pin,
|
||||
sync::{Arc, RwLock},
|
||||
time::{Duration, Instant},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use futures_core::Future;
|
||||
|
@ -11,10 +12,10 @@ use mango_v4::{
|
|||
i80f48::ClampToInt,
|
||||
state::{Bank, MangoAccountValue, TokenConditionalSwap, TokenIndex},
|
||||
};
|
||||
use mango_v4_client::{chain_data, health_cache, jupiter, MangoClient, TransactionBuilder};
|
||||
use mango_v4_client::{chain_data, jupiter, MangoClient, TransactionBuilder};
|
||||
|
||||
use anyhow::Context as AnyhowContext;
|
||||
use solana_sdk::{signature::Signature, signer::Signer};
|
||||
use solana_sdk::signature::Signature;
|
||||
use tracing::*;
|
||||
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
|
@ -56,7 +57,6 @@ pub enum Mode {
|
|||
pub struct Config {
|
||||
pub min_health_ratio: f64,
|
||||
pub max_trigger_quote_amount: u64,
|
||||
pub refresh_timeout: Duration,
|
||||
pub compute_limit_for_trigger: u32,
|
||||
pub collateral_token_index: TokenIndex,
|
||||
|
||||
|
@ -73,6 +73,9 @@ pub struct Config {
|
|||
pub jupiter_version: jupiter::Version,
|
||||
pub jupiter_slippage_bps: u64,
|
||||
pub mode: Mode,
|
||||
|
||||
pub only_allowed_tokens: HashSet<TokenIndex>,
|
||||
pub forbidden_tokens: HashSet<TokenIndex>,
|
||||
}
|
||||
|
||||
pub enum JupiterQuoteCacheResult<T> {
|
||||
|
@ -401,11 +404,43 @@ impl Context {
|
|||
Ok(taker_price >= base_price * cost_over_oracle * (1.0 + self.config.profit_fraction))
|
||||
}
|
||||
|
||||
// excluded by config
|
||||
fn tcs_pair_is_allowed(
|
||||
&self,
|
||||
buy_token_index: TokenIndex,
|
||||
sell_token_index: TokenIndex,
|
||||
) -> bool {
|
||||
if self.config.forbidden_tokens.contains(&buy_token_index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.config.forbidden_tokens.contains(&sell_token_index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.config.only_allowed_tokens.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.config.only_allowed_tokens.contains(&buy_token_index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.config.only_allowed_tokens.contains(&sell_token_index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Either expired or triggerable with ok-looking price.
|
||||
fn tcs_is_interesting(&self, tcs: &TokenConditionalSwap) -> anyhow::Result<bool> {
|
||||
if tcs.is_expired(self.now_ts) {
|
||||
return Ok(true);
|
||||
}
|
||||
if !self.tcs_pair_is_allowed(tcs.buy_token_index, tcs.buy_token_index) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let (_, buy_token_price, _) = self.token_bank_price_mint(tcs.buy_token_index)?;
|
||||
let (_, sell_token_price, _) = self.token_bank_price_mint(tcs.sell_token_index)?;
|
||||
|
@ -665,8 +700,9 @@ impl Context {
|
|||
liqee_old: &MangoAccountValue,
|
||||
tcs_id: u64,
|
||||
) -> anyhow::Result<Option<PreparedExecution>> {
|
||||
let fetcher = self.account_fetcher.as_ref();
|
||||
let health_cache = health_cache::new(&self.mango_client.context, fetcher, liqee_old)
|
||||
let health_cache = self
|
||||
.mango_client
|
||||
.health_cache(liqee_old)
|
||||
.await
|
||||
.context("creating health cache 1")?;
|
||||
if health_cache.is_liquidatable() {
|
||||
|
@ -685,7 +721,9 @@ impl Context {
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
let health_cache = health_cache::new(&self.mango_client.context, fetcher, &liqee)
|
||||
let health_cache = self
|
||||
.mango_client
|
||||
.health_cache(&liqee)
|
||||
.await
|
||||
.context("creating health cache 2")?;
|
||||
if health_cache.is_liquidatable() {
|
||||
|
@ -1165,10 +1203,8 @@ impl Context {
|
|||
let fee_payer = self.mango_client.client.fee_payer();
|
||||
TransactionBuilder {
|
||||
instructions: vec![compute_ix],
|
||||
address_lookup_tables: vec![],
|
||||
payer: fee_payer.pubkey(),
|
||||
signers: vec![self.mango_client.owner.clone(), fee_payer],
|
||||
config: self.mango_client.client.transaction_builder_config,
|
||||
..self.mango_client.transaction_builder().await?
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
use anchor_lang::error::Error::AnchorError;
|
||||
use mango_v4::error::MangoError;
|
||||
use mango_v4::state::TokenIndex;
|
||||
use regex::Regex;
|
||||
|
||||
pub trait UnwrappableOracleError {
|
||||
fn try_unwrap_oracle_error(&self) -> Option<(TokenIndex, String)>;
|
||||
}
|
||||
|
||||
impl UnwrappableOracleError for anyhow::Error {
|
||||
fn try_unwrap_oracle_error(&self) -> Option<(TokenIndex, String)> {
|
||||
let root_cause = self
|
||||
.root_cause()
|
||||
.downcast_ref::<anchor_lang::error::Error>();
|
||||
|
||||
if root_cause.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let AnchorError(ae) = root_cause.unwrap() {
|
||||
let is_oracle_error = ae.error_code_number == MangoError::OracleConfidence.error_code()
|
||||
|| ae.error_code_number == MangoError::OracleStale.error_code();
|
||||
|
||||
if !is_oracle_error {
|
||||
return None;
|
||||
}
|
||||
|
||||
let error_str = ae.to_string();
|
||||
return parse_oracle_error_string(&error_str);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_oracle_error_string(error_str: &str) -> Option<(TokenIndex, String)> {
|
||||
let token_name_regex = Regex::new(r#"name: (\w+)"#).unwrap();
|
||||
let token_index_regex = Regex::new(r#"token index (\d+)"#).unwrap();
|
||||
let token_name = token_name_regex
|
||||
.captures(error_str)
|
||||
.map(|c| c[1].to_string())
|
||||
.unwrap_or_default();
|
||||
let token_index = token_index_regex
|
||||
.captures(error_str)
|
||||
.map(|c| c[1].parse::<u16>().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
if token_index.is_some() {
|
||||
return Some((TokenIndex::from(token_index.unwrap()), token_name));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anchor_lang::error;
|
||||
use anyhow::Context;
|
||||
use mango_v4::error::Contextable;
|
||||
use mango_v4::error::MangoError;
|
||||
use mango_v4::state::{oracle_log_context, OracleConfig, OracleState, OracleType};
|
||||
|
||||
fn generate_errored_res() -> std::result::Result<u8, error::Error> {
|
||||
return Err(MangoError::OracleConfidence.into());
|
||||
}
|
||||
|
||||
fn generate_errored_res_with_context() -> anyhow::Result<u8> {
|
||||
let value = Contextable::with_context(
|
||||
Contextable::with_context(generate_errored_res(), || {
|
||||
oracle_log_context(
|
||||
"SOL",
|
||||
&OracleState {
|
||||
price: Default::default(),
|
||||
deviation: Default::default(),
|
||||
last_update_slot: 0,
|
||||
oracle_type: OracleType::Pyth,
|
||||
},
|
||||
&OracleConfig {
|
||||
conf_filter: Default::default(),
|
||||
max_staleness_slots: 0,
|
||||
reserved: [0; 72],
|
||||
},
|
||||
None,
|
||||
)
|
||||
}),
|
||||
|| {
|
||||
format!(
|
||||
"getting oracle for bank with health account index {} and token index {}, passed account {}",
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_extract_oracle_error_and_token_infos() {
|
||||
let error = generate_errored_res_with_context()
|
||||
.context("Something")
|
||||
.unwrap_err();
|
||||
println!("{}", error);
|
||||
println!("{}", error.root_cause());
|
||||
let oracle_error_opt = error.try_unwrap_oracle_error();
|
||||
|
||||
assert!(oracle_error_opt.is_some());
|
||||
assert_eq!(
|
||||
oracle_error_opt.unwrap(),
|
||||
(TokenIndex::from(11u16), "SOL".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_parse_oracle_error_message() {
|
||||
assert!(parse_oracle_error_string("").is_none());
|
||||
assert!(parse_oracle_error_string("Something went wrong").is_none());
|
||||
assert_eq!(
|
||||
parse_oracle_error_string("Something went wrong token index 4, name: SOL, Stale")
|
||||
.unwrap(),
|
||||
(TokenIndex::from(4u16), "SOL".to_string())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -78,7 +78,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
);
|
||||
let group_pk = Pubkey::from_str(&config.mango_group).unwrap();
|
||||
let group_context =
|
||||
Arc::new(MangoGroupContext::new_from_rpc(&client.rpc_async(), group_pk).await?);
|
||||
Arc::new(MangoGroupContext::new_from_rpc(client.rpc_async(), group_pk).await?);
|
||||
|
||||
let perp_queue_pks: Vec<_> = group_context
|
||||
.perp_markets
|
||||
|
|
|
@ -373,7 +373,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
);
|
||||
let group_context = Arc::new(
|
||||
MangoGroupContext::new_from_rpc(
|
||||
&client.rpc_async(),
|
||||
client.rpc_async(),
|
||||
Pubkey::from_str(&config.mango_group).unwrap(),
|
||||
)
|
||||
.await?,
|
||||
|
|
|
@ -357,7 +357,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
);
|
||||
let group_context = Arc::new(
|
||||
MangoGroupContext::new_from_rpc(
|
||||
&client.rpc_async(),
|
||||
client.rpc_async(),
|
||||
Pubkey::from_str(&config.mango_group).unwrap(),
|
||||
)
|
||||
.await?,
|
||||
|
|
|
@ -21,7 +21,8 @@ use fixed::types::I80F48;
|
|||
use mango_feeds_connector::metrics::*;
|
||||
use mango_v4::state::{MangoAccount, MangoAccountValue, PerpMarketIndex};
|
||||
use mango_v4_client::{
|
||||
chain_data, health_cache, AccountFetcher, Client, MangoGroupContext, TransactionBuilderConfig,
|
||||
chain_data, health_cache, AccountFetcher, Client, FallbackOracleConfig, MangoGroupContext,
|
||||
TransactionBuilderConfig,
|
||||
};
|
||||
use solana_sdk::commitment_config::CommitmentConfig;
|
||||
use solana_sdk::{account::ReadableAccount, signature::Keypair};
|
||||
|
@ -52,7 +53,13 @@ async fn compute_pnl(
|
|||
account_fetcher: Arc<impl AccountFetcher>,
|
||||
account: &MangoAccountValue,
|
||||
) -> anyhow::Result<Vec<(PerpMarketIndex, I80F48)>> {
|
||||
let health_cache = health_cache::new(&context, account_fetcher.as_ref(), account).await?;
|
||||
let health_cache = health_cache::new(
|
||||
&context,
|
||||
&FallbackOracleConfig::Dynamic,
|
||||
account_fetcher.as_ref(),
|
||||
account,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let pnls = account
|
||||
.active_perp_positions()
|
||||
|
@ -265,7 +272,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
);
|
||||
let group_context = Arc::new(
|
||||
MangoGroupContext::new_from_rpc(
|
||||
&client.rpc_async(),
|
||||
client.rpc_async(),
|
||||
Pubkey::from_str(&config.pnl.mango_group).unwrap(),
|
||||
)
|
||||
.await?,
|
||||
|
@ -273,7 +280,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
let chain_data = Arc::new(RwLock::new(chain_data::ChainData::new()));
|
||||
let account_fetcher = Arc::new(chain_data::AccountFetcher {
|
||||
chain_data: chain_data.clone(),
|
||||
rpc: client.rpc_async(),
|
||||
rpc: client.new_rpc_async(),
|
||||
});
|
||||
|
||||
let metrics_tx = metrics::start(config.metrics, "pnl".into());
|
||||
|
|
|
@ -6,8 +6,8 @@ use anchor_client::Cluster;
|
|||
use clap::Parser;
|
||||
use mango_v4::state::{PerpMarketIndex, TokenIndex};
|
||||
use mango_v4_client::{
|
||||
account_update_stream, chain_data, keypair_from_cli, snapshot_source, websocket_source, Client,
|
||||
MangoClient, MangoGroupContext, TransactionBuilderConfig,
|
||||
account_update_stream, chain_data, keypair_from_cli, priority_fees_cli, snapshot_source,
|
||||
websocket_source, Client, MangoClient, MangoGroupContext, TransactionBuilderConfig,
|
||||
};
|
||||
use tracing::*;
|
||||
|
||||
|
@ -61,9 +61,12 @@ struct Cli {
|
|||
#[clap(long, env, default_value = "100")]
|
||||
get_multiple_accounts_count: usize,
|
||||
|
||||
/// prioritize each transaction with this many microlamports/cu
|
||||
#[clap(long, env, default_value = "0")]
|
||||
prioritization_micro_lamports: u64,
|
||||
#[clap(flatten)]
|
||||
prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs,
|
||||
|
||||
/// url to the lite-rpc websocket, optional
|
||||
#[clap(long, env, default_value = "")]
|
||||
lite_rpc_url: String,
|
||||
|
||||
/// compute budget for each instruction
|
||||
#[clap(long, env, default_value = "250000")]
|
||||
|
@ -87,6 +90,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
};
|
||||
let cli = Cli::parse_from(args);
|
||||
|
||||
let (prio_provider, prio_jobs) = cli
|
||||
.prioritization_fee_cli
|
||||
.make_prio_provider(cli.lite_rpc_url.clone())?;
|
||||
|
||||
let settler_owner = Arc::new(keypair_from_cli(&cli.settler_owner));
|
||||
|
||||
let rpc_url = cli.rpc_url;
|
||||
|
@ -100,11 +107,11 @@ async fn main() -> anyhow::Result<()> {
|
|||
commitment,
|
||||
settler_owner.clone(),
|
||||
Some(rpc_timeout),
|
||||
TransactionBuilderConfig {
|
||||
prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0)
|
||||
.then_some(cli.prioritization_micro_lamports),
|
||||
compute_budget_per_instruction: Some(cli.compute_budget_per_instruction),
|
||||
},
|
||||
TransactionBuilderConfig::builder()
|
||||
.compute_budget_per_instruction(Some(cli.compute_budget_per_instruction))
|
||||
.priority_fee_provider(prio_provider)
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// The representation of current on-chain account data
|
||||
|
@ -112,7 +119,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
// Reading accounts from chain_data
|
||||
let account_fetcher = Arc::new(chain_data::AccountFetcher {
|
||||
chain_data: chain_data.clone(),
|
||||
rpc: client.rpc_async(),
|
||||
rpc: client.new_rpc_async(),
|
||||
});
|
||||
|
||||
let mango_account = account_fetcher
|
||||
|
@ -120,7 +127,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
.await?;
|
||||
let mango_group = mango_account.fixed.group;
|
||||
|
||||
let group_context = MangoGroupContext::new_from_rpc(&client.rpc_async(), mango_group).await?;
|
||||
let group_context = MangoGroupContext::new_from_rpc(client.rpc_async(), mango_group).await?;
|
||||
|
||||
let mango_oracles = group_context
|
||||
.tokens
|
||||
|
@ -352,6 +359,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
check_changes_for_abort_job,
|
||||
]
|
||||
.into_iter()
|
||||
.chain(prio_jobs.into_iter())
|
||||
.collect();
|
||||
jobs.next().await;
|
||||
|
||||
|
|
|
@ -5,10 +5,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|||
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||||
use mango_v4::health::HealthType;
|
||||
use mango_v4::state::{OracleAccountInfos, PerpMarket, PerpMarketIndex};
|
||||
use mango_v4_client::{
|
||||
chain_data, health_cache, prettify_solana_client_error, MangoClient, PreparedInstructions,
|
||||
TransactionBuilder,
|
||||
};
|
||||
use mango_v4_client::{chain_data, MangoClient, PreparedInstructions, TransactionBuilder};
|
||||
use solana_sdk::address_lookup_table_account::AddressLookupTableAccount;
|
||||
use solana_sdk::commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Signature;
|
||||
|
@ -120,11 +117,10 @@ impl SettlementState {
|
|||
continue;
|
||||
}
|
||||
|
||||
let health_cache =
|
||||
match health_cache::new(&mango_client.context, account_fetcher, &account).await {
|
||||
Ok(hc) => hc,
|
||||
Err(_) => continue, // Skip for stale/unconfident oracles
|
||||
};
|
||||
let health_cache = match mango_client.health_cache(&account).await {
|
||||
Ok(hc) => hc,
|
||||
Err(_) => continue, // Skip for stale/unconfident oracles
|
||||
};
|
||||
let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
|
||||
|
||||
for perp_market_index in perp_indexes {
|
||||
|
@ -288,7 +284,7 @@ impl<'a> SettleBatchProcessor<'a> {
|
|||
address_lookup_tables: self.address_lookup_tables.clone(),
|
||||
payer: fee_payer.pubkey(),
|
||||
signers: vec![fee_payer],
|
||||
config: client.transaction_builder_config,
|
||||
config: client.config().transaction_builder_config.clone(),
|
||||
}
|
||||
.transaction_with_blockhash(self.blockhash)
|
||||
}
|
||||
|
@ -301,13 +297,7 @@ impl<'a> SettleBatchProcessor<'a> {
|
|||
let tx = self.transaction()?;
|
||||
self.instructions.clear();
|
||||
|
||||
let send_result = self
|
||||
.mango_client
|
||||
.client
|
||||
.rpc_async()
|
||||
.send_transaction_with_config(&tx, self.mango_client.client.rpc_send_transaction_config)
|
||||
.await
|
||||
.map_err(prettify_solana_client_error);
|
||||
let send_result = self.mango_client.client.send_transaction(&tx).await;
|
||||
|
||||
match send_result {
|
||||
Ok(txsig) => {
|
||||
|
@ -328,11 +318,14 @@ impl<'a> SettleBatchProcessor<'a> {
|
|||
) -> anyhow::Result<Option<Signature>> {
|
||||
let a_value = self.account_fetcher.fetch_mango_account(&account_a)?;
|
||||
let b_value = self.account_fetcher.fetch_mango_account(&account_b)?;
|
||||
let new_ixs = self.mango_client.perp_settle_pnl_instruction(
|
||||
self.perp_market_index,
|
||||
(&account_a, &a_value),
|
||||
(&account_b, &b_value),
|
||||
)?;
|
||||
let new_ixs = self
|
||||
.mango_client
|
||||
.perp_settle_pnl_instruction(
|
||||
self.perp_market_index,
|
||||
(&account_a, &a_value),
|
||||
(&account_b, &b_value),
|
||||
)
|
||||
.await?;
|
||||
let previous = self.instructions.clone();
|
||||
self.instructions.append(new_ixs.clone());
|
||||
|
||||
|
|
|
@ -123,14 +123,17 @@ impl State {
|
|||
}
|
||||
|
||||
// Clear newly created token positions, so the liqor account is mostly empty
|
||||
for token_index in startable_chunk.iter().map(|(_, _, ti)| *ti).unique() {
|
||||
let new_token_pos_indices = startable_chunk
|
||||
.iter()
|
||||
.map(|(_, _, ti)| *ti)
|
||||
.unique()
|
||||
.collect_vec();
|
||||
for token_index in new_token_pos_indices {
|
||||
let mint = mango_client.context.token(token_index).mint;
|
||||
let ix = match mango_client.token_withdraw_instructions(
|
||||
&liqor_account,
|
||||
mint,
|
||||
u64::MAX,
|
||||
false,
|
||||
) {
|
||||
let ix = match mango_client
|
||||
.token_withdraw_instructions(&liqor_account, mint, u64::MAX, false)
|
||||
.await
|
||||
{
|
||||
Ok(ix) => ix,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ async-channel = "1.6"
|
|||
async-once-cell = { version = "0.4.2", features = ["unpin"] }
|
||||
async-trait = "0.1.52"
|
||||
atty = "0.2"
|
||||
clap = { version = "3.1.8", features = ["derive", "env"] }
|
||||
derive_builder = "0.12.0"
|
||||
fixed = { workspace = true, features = ["serde", "borsh"] }
|
||||
futures = "0.3.25"
|
||||
|
@ -30,6 +31,7 @@ solana-client = { workspace = true }
|
|||
solana-rpc = { workspace = true }
|
||||
solana-sdk = { workspace = true }
|
||||
solana-address-lookup-table-program = { workspace = true }
|
||||
solana-transaction-status = { workspace = true }
|
||||
mango-feeds-connector = { workspace = true }
|
||||
spl-associated-token-account = "1.0.3"
|
||||
thiserror = "1.0.31"
|
||||
|
@ -37,6 +39,7 @@ thiserror = "1.0.31"
|
|||
reqwest = "0.11.17"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = { version = "0.1.9"}
|
||||
tokio-tungstenite = "0.17.0"
|
||||
serde = "1.0.141"
|
||||
serde_json = "1.0.82"
|
||||
base64 = "0.13.0"
|
||||
|
|
|
@ -11,10 +11,14 @@ use anchor_lang::AccountDeserialize;
|
|||
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_sdk::account::{AccountSharedData, ReadableAccount};
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::hash::Hasher;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
use mango_v4::state::MangoAccountValue;
|
||||
|
||||
use crate::gpa;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait AccountFetcher: Sync + Send {
|
||||
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
|
||||
|
@ -29,6 +33,13 @@ pub trait AccountFetcher: Sync + Send {
|
|||
program: &Pubkey,
|
||||
discriminator: [u8; 8],
|
||||
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>>;
|
||||
|
||||
async fn fetch_multiple_accounts(
|
||||
&self,
|
||||
keys: &[Pubkey],
|
||||
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>>;
|
||||
|
||||
async fn get_slot(&self) -> anyhow::Result<u64>;
|
||||
}
|
||||
|
||||
// Can't be in the trait, since then it would no longer be object-safe...
|
||||
|
@ -100,6 +111,17 @@ impl AccountFetcher for RpcAccountFetcher {
|
|||
.map(|(pk, acc)| (pk, acc.into()))
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
async fn fetch_multiple_accounts(
|
||||
&self,
|
||||
keys: &[Pubkey],
|
||||
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
|
||||
gpa::fetch_multiple_accounts(&self.rpc, keys).await
|
||||
}
|
||||
|
||||
async fn get_slot(&self) -> anyhow::Result<u64> {
|
||||
Ok(self.rpc.get_slot().await?)
|
||||
}
|
||||
}
|
||||
|
||||
struct CoalescedAsyncJob<Key, Output> {
|
||||
|
@ -138,6 +160,8 @@ struct AccountCache {
|
|||
keys_for_program_and_discriminator: HashMap<(Pubkey, [u8; 8]), Vec<Pubkey>>,
|
||||
|
||||
account_jobs: CoalescedAsyncJob<Pubkey, anyhow::Result<AccountSharedData>>,
|
||||
multiple_accounts_jobs:
|
||||
CoalescedAsyncJob<Hash, anyhow::Result<Vec<(Pubkey, AccountSharedData)>>>,
|
||||
program_accounts_jobs:
|
||||
CoalescedAsyncJob<(Pubkey, [u8; 8]), anyhow::Result<Vec<(Pubkey, AccountSharedData)>>>,
|
||||
}
|
||||
|
@ -261,4 +285,62 @@ impl<T: AccountFetcher + 'static> AccountFetcher for CachedAccountFetcher<T> {
|
|||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_multiple_accounts(
|
||||
&self,
|
||||
keys: &[Pubkey],
|
||||
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
|
||||
let fetch_job = {
|
||||
let mut cache = self.cache.lock().unwrap();
|
||||
let mut missing_keys: Vec<Pubkey> = keys
|
||||
.iter()
|
||||
.filter(|k| !cache.accounts.contains_key(k))
|
||||
.cloned()
|
||||
.collect();
|
||||
if missing_keys.len() == 0 {
|
||||
return Ok(keys
|
||||
.iter()
|
||||
.map(|pk| (*pk, cache.accounts.get(&pk).unwrap().clone()))
|
||||
.collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
let self_copy = self.clone();
|
||||
missing_keys.sort();
|
||||
let mut hasher = Hasher::default();
|
||||
for key in missing_keys.iter() {
|
||||
hasher.hash(key.as_ref());
|
||||
}
|
||||
let job_key = hasher.result();
|
||||
cache
|
||||
.multiple_accounts_jobs
|
||||
.run_coalesced(job_key.clone(), async move {
|
||||
let result = self_copy
|
||||
.fetcher
|
||||
.fetch_multiple_accounts(&missing_keys)
|
||||
.await;
|
||||
let mut cache = self_copy.cache.lock().unwrap();
|
||||
cache.multiple_accounts_jobs.remove(&job_key);
|
||||
|
||||
if let Ok(results) = result.as_ref() {
|
||||
for (key, account) in results {
|
||||
cache.accounts.insert(*key, account.clone());
|
||||
}
|
||||
}
|
||||
result
|
||||
})
|
||||
};
|
||||
|
||||
match fetch_job.get().await {
|
||||
Ok(v) => Ok(v.clone()),
|
||||
// Can't clone the stored error, so need to stringize it
|
||||
Err(err) => Err(anyhow::format_err!(
|
||||
"fetch error in CachedAccountFetcher: {:?}",
|
||||
err
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_slot(&self) -> anyhow::Result<u64> {
|
||||
self.fetcher.get_slot().await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,10 @@ use anchor_lang::Discriminator;
|
|||
|
||||
use fixed::types::I80F48;
|
||||
use mango_v4::accounts_zerocopy::{KeyedAccountSharedData, LoadZeroCopy};
|
||||
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, OracleAccountInfos};
|
||||
use mango_v4::state::{
|
||||
pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, Bank, MangoAccount, MangoAccountValue,
|
||||
OracleAccountInfos,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
|
@ -64,12 +67,34 @@ impl AccountFetcher {
|
|||
|
||||
pub fn fetch_bank_and_price(&self, bank: &Pubkey) -> anyhow::Result<(Bank, I80F48)> {
|
||||
let bank: Bank = self.fetch(bank)?;
|
||||
let oracle = self.fetch_raw(&bank.oracle)?;
|
||||
let oracle_acc = &KeyedAccountSharedData::new(bank.oracle, oracle.into());
|
||||
let price = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_acc), None)?;
|
||||
let oracle_data = self.fetch_raw(&bank.oracle)?;
|
||||
let oracle = &KeyedAccountSharedData::new(bank.oracle, oracle_data.into());
|
||||
|
||||
let fallback_opt = self.fetch_keyed_account_data(bank.fallback_oracle)?;
|
||||
let sol_opt = self.fetch_keyed_account_data(pyth_mainnet_sol_oracle::ID)?;
|
||||
let usdc_opt = self.fetch_keyed_account_data(pyth_mainnet_usdc_oracle::ID)?;
|
||||
|
||||
let oracle_acc_infos = OracleAccountInfos {
|
||||
oracle,
|
||||
fallback_opt: fallback_opt.as_ref(),
|
||||
usdc_opt: usdc_opt.as_ref(),
|
||||
sol_opt: sol_opt.as_ref(),
|
||||
};
|
||||
let price = bank.oracle_price(&oracle_acc_infos, None)?;
|
||||
Ok((bank, price))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fetch_keyed_account_data(
|
||||
&self,
|
||||
key: Pubkey,
|
||||
) -> anyhow::Result<Option<KeyedAccountSharedData>> {
|
||||
Ok(self
|
||||
.fetch_raw(&key)
|
||||
.ok()
|
||||
.map(|data| KeyedAccountSharedData::new(key, data)))
|
||||
}
|
||||
|
||||
pub fn fetch_bank_price(&self, bank: &Pubkey) -> anyhow::Result<I80F48> {
|
||||
self.fetch_bank_and_price(bank).map(|(_, p)| p)
|
||||
}
|
||||
|
@ -217,4 +242,20 @@ impl crate::AccountFetcher for AccountFetcher {
|
|||
})
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
async fn fetch_multiple_accounts(
|
||||
&self,
|
||||
keys: &[Pubkey],
|
||||
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
|
||||
let chain_data = self.chain_data.read().unwrap();
|
||||
Ok(keys
|
||||
.iter()
|
||||
.map(|pk| (*pk, chain_data.account(pk).unwrap().account.clone()))
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
async fn get_slot(&self) -> anyhow::Result<u64> {
|
||||
let chain_data = self.chain_data.read().unwrap();
|
||||
Ok(chain_data.newest_processed_slot())
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,117 @@
|
|||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_client::rpc_request::RpcError;
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, signature::Signature};
|
||||
use solana_transaction_status::TransactionStatus;
|
||||
|
||||
use crate::util::delay_interval;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum WaitForTransactionConfirmationError {
|
||||
#[error("blockhash has expired")]
|
||||
BlockhashExpired,
|
||||
#[error("timeout expired")]
|
||||
Timeout,
|
||||
#[error("client error: {0:?}")]
|
||||
ClientError(#[from] solana_client::client_error::ClientError),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Builder)]
|
||||
#[builder(default)]
|
||||
pub struct RpcConfirmTransactionConfig {
|
||||
/// If none, defaults to the RpcClient's configured default commitment.
|
||||
pub commitment: Option<CommitmentConfig>,
|
||||
|
||||
/// Time after which to start checking for blockhash expiry.
|
||||
pub recent_blockhash_initial_timeout: Duration,
|
||||
|
||||
/// Interval between signature status queries.
|
||||
pub signature_status_interval: Duration,
|
||||
|
||||
/// If none, there's no timeout. The confirmation will still abort eventually
|
||||
/// when the blockhash expires.
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for RpcConfirmTransactionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
commitment: None,
|
||||
recent_blockhash_initial_timeout: Duration::from_secs(5),
|
||||
signature_status_interval: Duration::from_millis(500),
|
||||
timeout: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcConfirmTransactionConfig {
|
||||
pub fn builder() -> RpcConfirmTransactionConfigBuilder {
|
||||
RpcConfirmTransactionConfigBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for `signature` to be confirmed at `commitment` or until either
|
||||
/// - `recent_blockhash` is so old that the tx can't be confirmed _and_
|
||||
/// `blockhash_initial_timeout` is reached
|
||||
/// - the `signature_status_timeout` is reached
|
||||
/// While waiting, query for confirmation every `signature_status_interval`
|
||||
///
|
||||
/// NOTE: RpcClient::config contains confirm_transaction_initial_timeout which is the
|
||||
/// same as blockhash_initial_timeout. Unfortunately the former is private.
|
||||
///
|
||||
/// Returns:
|
||||
/// - blockhash and blockhash_initial_timeout expired -> BlockhashExpired error
|
||||
/// - signature_status_timeout expired -> Timeout error (possibly just didn't reach commitment in time?)
|
||||
/// - any rpc error -> ClientError error
|
||||
/// - confirmed at commitment -> ok(slot, opt<tx_error>)
|
||||
pub async fn wait_for_transaction_confirmation(
|
||||
rpc_client: &RpcClientAsync,
|
||||
signature: &Signature,
|
||||
recent_blockhash: &solana_sdk::hash::Hash,
|
||||
config: &RpcConfirmTransactionConfig,
|
||||
) -> Result<TransactionStatus, WaitForTransactionConfirmationError> {
|
||||
let mut signature_status_interval = delay_interval(config.signature_status_interval);
|
||||
let commitment = config.commitment.unwrap_or(rpc_client.commitment());
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let is_timed_out = || config.timeout.map(|t| start.elapsed() > t).unwrap_or(false);
|
||||
loop {
|
||||
signature_status_interval.tick().await;
|
||||
if is_timed_out() {
|
||||
return Err(WaitForTransactionConfirmationError::Timeout);
|
||||
}
|
||||
|
||||
let statuses = rpc_client
|
||||
.get_signature_statuses(&[signature.clone()])
|
||||
.await?;
|
||||
let status_opt = match statuses.value.into_iter().next() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(WaitForTransactionConfirmationError::ClientError(
|
||||
RpcError::ParseError(
|
||||
"must contain an entry for each requested signature".into(),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// If the tx isn't seen at all (not even processed), check blockhash expiry
|
||||
if status_opt.is_none() {
|
||||
if start.elapsed() > config.recent_blockhash_initial_timeout {
|
||||
let blockhash_is_valid = rpc_client
|
||||
.is_blockhash_valid(recent_blockhash, CommitmentConfig::processed())
|
||||
.await?;
|
||||
if !blockhash_is_valid {
|
||||
return Err(WaitForTransactionConfirmationError::BlockhashExpired);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = status_opt.unwrap();
|
||||
if status.satisfies_commitment(commitment) {
|
||||
return Ok(status);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,15 +4,20 @@ use anchor_client::ClientError;
|
|||
|
||||
use anchor_lang::__private::bytemuck;
|
||||
|
||||
use mango_v4::state::{
|
||||
Group, MangoAccountValue, PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS,
|
||||
use mango_v4::{
|
||||
accounts_zerocopy::{KeyedAccountReader, KeyedAccountSharedData},
|
||||
state::{
|
||||
determine_oracle_type, load_whirlpool_state, oracle_state_unchecked, Group,
|
||||
MangoAccountValue, OracleAccountInfos, OracleConfig, OracleConfigParams, OracleType,
|
||||
PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS,
|
||||
},
|
||||
};
|
||||
|
||||
use fixed::types::I80F48;
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::gpa::*;
|
||||
use crate::{gpa::*, AccountFetcher, FallbackOracleConfig};
|
||||
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_sdk::account::Account;
|
||||
|
@ -28,9 +33,10 @@ pub struct TokenContext {
|
|||
pub oracle: Pubkey,
|
||||
pub banks: [Pubkey; MAX_BANKS],
|
||||
pub vaults: [Pubkey; MAX_BANKS],
|
||||
pub fallback_oracle: Pubkey,
|
||||
pub fallback_context: FallbackOracleContext,
|
||||
pub mint_info_address: Pubkey,
|
||||
pub decimals: u8,
|
||||
pub oracle_config: OracleConfig,
|
||||
}
|
||||
|
||||
impl TokenContext {
|
||||
|
@ -56,6 +62,18 @@ impl TokenContext {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct FallbackOracleContext {
|
||||
pub key: Pubkey,
|
||||
// only used for CLMM fallback oracles, otherwise Pubkey::default
|
||||
pub quote_key: Pubkey,
|
||||
}
|
||||
impl FallbackOracleContext {
|
||||
pub fn keys(&self) -> Vec<Pubkey> {
|
||||
vec![self.key, self.quote_key]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Serum3MarketContext {
|
||||
pub address: Pubkey,
|
||||
|
@ -101,6 +119,9 @@ pub struct ComputeEstimates {
|
|||
pub cu_per_serum3_order_cancel: u32,
|
||||
pub cu_per_perp_order_match: u32,
|
||||
pub cu_per_perp_order_cancel: u32,
|
||||
pub cu_per_oracle_fallback: u32,
|
||||
pub cu_per_charge_collateral_fees: u32,
|
||||
pub cu_per_charge_collateral_fees_token: u32,
|
||||
}
|
||||
|
||||
impl Default for ComputeEstimates {
|
||||
|
@ -118,25 +139,40 @@ impl Default for ComputeEstimates {
|
|||
cu_per_perp_order_match: 7_000,
|
||||
// measured around 3.5k, see test_perp_compute
|
||||
cu_per_perp_order_cancel: 7_000,
|
||||
// measured around 2k, see test_health_compute_tokens_fallback_oracles
|
||||
cu_per_oracle_fallback: 2000,
|
||||
// the base cost is mostly the division
|
||||
cu_per_charge_collateral_fees: 20_000,
|
||||
// per-chargable-token cost
|
||||
cu_per_charge_collateral_fees_token: 15_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComputeEstimates {
|
||||
pub fn health_for_counts(&self, tokens: usize, perps: usize, serums: usize) -> u32 {
|
||||
pub fn health_for_counts(
|
||||
&self,
|
||||
tokens: usize,
|
||||
perps: usize,
|
||||
serums: usize,
|
||||
fallbacks: usize,
|
||||
) -> u32 {
|
||||
let tokens: u32 = tokens.try_into().unwrap();
|
||||
let perps: u32 = perps.try_into().unwrap();
|
||||
let serums: u32 = serums.try_into().unwrap();
|
||||
let fallbacks: u32 = fallbacks.try_into().unwrap();
|
||||
tokens * self.health_cu_per_token
|
||||
+ perps * self.health_cu_per_perp
|
||||
+ serums * self.health_cu_per_serum
|
||||
+ fallbacks * self.cu_per_oracle_fallback
|
||||
}
|
||||
|
||||
pub fn health_for_account(&self, account: &MangoAccountValue) -> u32 {
|
||||
pub fn health_for_account(&self, account: &MangoAccountValue, num_fallbacks: usize) -> u32 {
|
||||
self.health_for_counts(
|
||||
account.active_token_positions().count(),
|
||||
account.active_perp_positions().count(),
|
||||
account.active_serum3_orders().count(),
|
||||
num_fallbacks,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -227,8 +263,12 @@ impl MangoGroupContext {
|
|||
decimals: u8::MAX,
|
||||
banks: mi.banks,
|
||||
vaults: mi.vaults,
|
||||
fallback_oracle: mi.fallback_oracle,
|
||||
oracle: mi.oracle,
|
||||
fallback_context: FallbackOracleContext {
|
||||
key: mi.fallback_oracle,
|
||||
quote_key: Pubkey::default(),
|
||||
},
|
||||
oracle_config: OracleConfigParams::default().to_oracle_config(),
|
||||
group: mi.group,
|
||||
mint: mi.mint,
|
||||
},
|
||||
|
@ -236,14 +276,23 @@ impl MangoGroupContext {
|
|||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// reading the banks is only needed for the token names and decimals
|
||||
// reading the banks is only needed for the token names, decimals and oracle configs
|
||||
// FUTURE: either store the names on MintInfo as well, or maybe don't store them at all
|
||||
// because they are in metaplex?
|
||||
let bank_tuples = fetch_banks(rpc, program, group).await?;
|
||||
for (_, bank) in bank_tuples {
|
||||
let fallback_keys: Vec<Pubkey> = bank_tuples
|
||||
.iter()
|
||||
.map(|tup| tup.1.fallback_oracle)
|
||||
.collect();
|
||||
let fallback_oracle_accounts = fetch_multiple_accounts(rpc, &fallback_keys[..]).await?;
|
||||
for (index, (_, bank)) in bank_tuples.iter().enumerate() {
|
||||
let token = tokens.get_mut(&bank.token_index).unwrap();
|
||||
token.name = bank.name().into();
|
||||
token.decimals = bank.mint_decimals;
|
||||
token.oracle_config = bank.oracle_config;
|
||||
let (key, acc_info) = fallback_oracle_accounts[index].clone();
|
||||
token.fallback_context.quote_key =
|
||||
get_fallback_quote_key(&KeyedAccountSharedData::new(key, acc_info));
|
||||
}
|
||||
assert!(tokens.values().all(|t| t.decimals != u8::MAX));
|
||||
|
||||
|
@ -357,6 +406,7 @@ impl MangoGroupContext {
|
|||
affected_tokens: Vec<TokenIndex>,
|
||||
writable_banks: Vec<TokenIndex>,
|
||||
affected_perp_markets: Vec<PerpMarketIndex>,
|
||||
fallback_contexts: HashMap<Pubkey, FallbackOracleContext>,
|
||||
) -> anyhow::Result<(Vec<AccountMeta>, u32)> {
|
||||
let mut account = account.clone();
|
||||
for affected_token_index in affected_tokens.iter().chain(writable_banks.iter()) {
|
||||
|
@ -370,6 +420,7 @@ impl MangoGroupContext {
|
|||
// figure out all the banks/oracles that need to be passed for the health check
|
||||
let mut banks = vec![];
|
||||
let mut oracles = vec![];
|
||||
let mut fallbacks = vec![];
|
||||
for position in account.active_token_positions() {
|
||||
let token = self.token(position.token_index);
|
||||
banks.push((
|
||||
|
@ -377,6 +428,9 @@ impl MangoGroupContext {
|
|||
writable_banks.iter().any(|&ti| ti == position.token_index),
|
||||
));
|
||||
oracles.push(token.oracle);
|
||||
if let Some(fallback_context) = fallback_contexts.get(&token.oracle) {
|
||||
fallbacks.extend(fallback_context.keys());
|
||||
}
|
||||
}
|
||||
|
||||
let serum_oos = account.active_serum3_orders().map(|&s| s.open_orders);
|
||||
|
@ -386,6 +440,14 @@ impl MangoGroupContext {
|
|||
let perp_oracles = account
|
||||
.active_perp_positions()
|
||||
.map(|&pa| self.perp(pa.market_index).oracle);
|
||||
// FUTURE: implement fallback oracles for perps
|
||||
|
||||
let fallback_oracles: Vec<Pubkey> = fallbacks
|
||||
.into_iter()
|
||||
.unique()
|
||||
.filter(|key| !oracles.contains(key) && key != &Pubkey::default())
|
||||
.collect();
|
||||
let fallbacks_len = fallback_oracles.len();
|
||||
|
||||
let to_account_meta = |pubkey| AccountMeta {
|
||||
pubkey,
|
||||
|
@ -404,9 +466,12 @@ impl MangoGroupContext {
|
|||
.chain(perp_markets.map(to_account_meta))
|
||||
.chain(perp_oracles.map(to_account_meta))
|
||||
.chain(serum_oos.map(to_account_meta))
|
||||
.chain(fallback_oracles.into_iter().map(to_account_meta))
|
||||
.collect();
|
||||
|
||||
let cu = self.compute_estimates.health_for_account(&account);
|
||||
let cu = self
|
||||
.compute_estimates
|
||||
.health_for_account(&account, fallbacks_len);
|
||||
|
||||
Ok((accounts, cu))
|
||||
}
|
||||
|
@ -417,10 +482,12 @@ impl MangoGroupContext {
|
|||
account2: &MangoAccountValue,
|
||||
affected_tokens: &[TokenIndex],
|
||||
writable_banks: &[TokenIndex],
|
||||
fallback_contexts: HashMap<Pubkey, FallbackOracleContext>,
|
||||
) -> anyhow::Result<(Vec<AccountMeta>, u32)> {
|
||||
// figure out all the banks/oracles that need to be passed for the health check
|
||||
let mut banks = vec![];
|
||||
let mut oracles = vec![];
|
||||
let mut fallbacks = vec![];
|
||||
|
||||
let token_indexes = account2
|
||||
.active_token_positions()
|
||||
|
@ -434,6 +501,9 @@ impl MangoGroupContext {
|
|||
let writable_bank = writable_banks.iter().contains(&token_index);
|
||||
banks.push((token.first_bank(), writable_bank));
|
||||
oracles.push(token.oracle);
|
||||
if let Some(fallback_context) = fallback_contexts.get(&token.oracle) {
|
||||
fallbacks.extend(fallback_context.keys());
|
||||
}
|
||||
}
|
||||
|
||||
let serum_oos = account2
|
||||
|
@ -452,6 +522,14 @@ impl MangoGroupContext {
|
|||
let perp_oracles = perp_market_indexes
|
||||
.iter()
|
||||
.map(|&index| self.perp(index).oracle);
|
||||
// FUTURE: implement fallback oracles for perps
|
||||
|
||||
let fallback_oracles: Vec<Pubkey> = fallbacks
|
||||
.into_iter()
|
||||
.unique()
|
||||
.filter(|key| !oracles.contains(key) && key != &Pubkey::default())
|
||||
.collect();
|
||||
let fallbacks_len = fallback_oracles.len();
|
||||
|
||||
let to_account_meta = |pubkey| AccountMeta {
|
||||
pubkey,
|
||||
|
@ -470,6 +548,7 @@ impl MangoGroupContext {
|
|||
.chain(perp_markets.map(to_account_meta))
|
||||
.chain(perp_oracles.map(to_account_meta))
|
||||
.chain(serum_oos.map(to_account_meta))
|
||||
.chain(fallback_oracles.into_iter().map(to_account_meta))
|
||||
.collect();
|
||||
|
||||
// Since health is likely to be computed separately for both accounts, we don't use the
|
||||
|
@ -490,10 +569,12 @@ impl MangoGroupContext {
|
|||
account1_token_count,
|
||||
account1.active_perp_positions().count(),
|
||||
account1.active_serum3_orders().count(),
|
||||
fallbacks_len,
|
||||
) + self.compute_estimates.health_for_counts(
|
||||
account2_token_count,
|
||||
account2.active_perp_positions().count(),
|
||||
account2.active_serum3_orders().count(),
|
||||
fallbacks_len,
|
||||
);
|
||||
|
||||
Ok((accounts, cu))
|
||||
|
@ -554,6 +635,61 @@ impl MangoGroupContext {
|
|||
let new_perp_markets = fetch_perp_markets(rpc, mango_v4::id(), self.group).await?;
|
||||
Ok(new_perp_markets.len() > self.perp_markets.len())
|
||||
}
|
||||
|
||||
/// Returns a map of oracle pubkey -> FallbackOracleContext
|
||||
pub async fn derive_fallback_oracle_keys(
|
||||
&self,
|
||||
fallback_oracle_config: &FallbackOracleConfig,
|
||||
account_fetcher: &dyn AccountFetcher,
|
||||
) -> anyhow::Result<HashMap<Pubkey, FallbackOracleContext>> {
|
||||
// FUTURE: implement for perp oracles as well
|
||||
let fallbacks_by_oracle = match fallback_oracle_config {
|
||||
FallbackOracleConfig::Never => HashMap::new(),
|
||||
FallbackOracleConfig::Fixed(keys) => self
|
||||
.tokens
|
||||
.iter()
|
||||
.filter(|token| {
|
||||
token.1.fallback_context.key != Pubkey::default()
|
||||
&& keys.contains(&token.1.fallback_context.key)
|
||||
})
|
||||
.map(|t| (t.1.oracle, t.1.fallback_context.clone()))
|
||||
.collect(),
|
||||
FallbackOracleConfig::All => self
|
||||
.tokens
|
||||
.iter()
|
||||
.filter(|token| token.1.fallback_context.key != Pubkey::default())
|
||||
.map(|t| (t.1.oracle, t.1.fallback_context.clone()))
|
||||
.collect(),
|
||||
FallbackOracleConfig::Dynamic => {
|
||||
let tokens_by_oracle: HashMap<Pubkey, &TokenContext> =
|
||||
self.tokens.iter().map(|t| (t.1.oracle, t.1)).collect();
|
||||
let oracle_keys: Vec<Pubkey> =
|
||||
tokens_by_oracle.values().map(|b| b.oracle).collect();
|
||||
let oracle_accounts = account_fetcher
|
||||
.fetch_multiple_accounts(&oracle_keys)
|
||||
.await?;
|
||||
let now_slot = account_fetcher.get_slot().await?;
|
||||
|
||||
let mut stale_oracles_with_fallbacks = vec![];
|
||||
for (key, acc) in oracle_accounts {
|
||||
let token = tokens_by_oracle.get(&key).unwrap();
|
||||
let state = oracle_state_unchecked(
|
||||
&OracleAccountInfos::from_reader(&KeyedAccountSharedData::new(key, acc)),
|
||||
token.decimals,
|
||||
)?;
|
||||
let oracle_is_valid = state
|
||||
.check_confidence_and_maybe_staleness(&token.oracle_config, Some(now_slot));
|
||||
if oracle_is_valid.is_err() && token.fallback_context.key != Pubkey::default() {
|
||||
stale_oracles_with_fallbacks
|
||||
.push((token.oracle, token.fallback_context.clone()));
|
||||
}
|
||||
}
|
||||
stale_oracles_with_fallbacks.into_iter().collect()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(fallbacks_by_oracle)
|
||||
}
|
||||
}
|
||||
|
||||
fn from_serum_style_pubkey(d: [u64; 4]) -> Pubkey {
|
||||
|
@ -567,3 +703,22 @@ async fn fetch_raw_account(rpc: &RpcClientAsync, address: Pubkey) -> Result<Acco
|
|||
.value
|
||||
.ok_or(ClientError::AccountNotFound)
|
||||
}
|
||||
|
||||
/// Fetch the quote key for a fallback oracle account info.
|
||||
/// Returns Pubkey::default if no quote key is found or there are any
|
||||
/// errors occur when trying to fetch the quote oracle.
|
||||
/// This function will only return a non-default key when a CLMM oracle is used
|
||||
fn get_fallback_quote_key(acc_info: &impl KeyedAccountReader) -> Pubkey {
|
||||
let maybe_key = match determine_oracle_type(acc_info).ok() {
|
||||
Some(oracle_type) => match oracle_type {
|
||||
OracleType::OrcaCLMM => match load_whirlpool_state(acc_info).ok() {
|
||||
Some(whirlpool) => whirlpool.get_quote_oracle().ok(),
|
||||
None => None,
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
maybe_key.unwrap_or_else(|| Pubkey::default())
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ impl<Key> Default for ErrorTypeState<Key> {
|
|||
|
||||
#[derive(Builder)]
|
||||
pub struct ErrorTracking<Key, ErrorType> {
|
||||
#[builder(setter(custom))]
|
||||
#[builder(default, setter(custom))]
|
||||
errors_by_type: HashMap<ErrorType, ErrorTypeState<Key>>,
|
||||
|
||||
/// number of errors of a type after which had_too_many_errors returns true
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use anchor_lang::{AccountDeserialize, Discriminator};
|
||||
|
||||
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market};
|
||||
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
|
||||
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
|
||||
use solana_sdk::account::AccountSharedData;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub async fn fetch_mango_accounts(
|
||||
|
@ -129,3 +129,22 @@ pub async fn fetch_perp_markets(
|
|||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_multiple_accounts(
|
||||
rpc: &RpcClientAsync,
|
||||
keys: &[Pubkey],
|
||||
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
|
||||
let config = RpcAccountInfoConfig {
|
||||
encoding: Some(UiAccountEncoding::Base64),
|
||||
..RpcAccountInfoConfig::default()
|
||||
};
|
||||
Ok(rpc
|
||||
.get_multiple_accounts_with_config(keys, config)
|
||||
.await?
|
||||
.value
|
||||
.into_iter()
|
||||
.zip(keys.iter())
|
||||
.filter(|(maybe_acc, _)| maybe_acc.is_some())
|
||||
.map(|(acc, key)| (*key, acc.unwrap().into()))
|
||||
.collect())
|
||||
}
|
||||
|
|
|
@ -1,22 +1,32 @@
|
|||
use crate::{AccountFetcher, MangoGroupContext};
|
||||
use crate::{AccountFetcher, FallbackOracleConfig, MangoGroupContext};
|
||||
use anyhow::Context;
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||||
use mango_v4::health::{FixedOrderAccountRetriever, HealthCache};
|
||||
use mango_v4::state::MangoAccountValue;
|
||||
use mango_v4::state::{pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, MangoAccountValue};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub async fn new(
|
||||
context: &MangoGroupContext,
|
||||
account_fetcher: &impl AccountFetcher,
|
||||
fallback_config: &FallbackOracleConfig,
|
||||
account_fetcher: &dyn AccountFetcher,
|
||||
account: &MangoAccountValue,
|
||||
) -> anyhow::Result<HealthCache> {
|
||||
let active_token_len = account.active_token_positions().count();
|
||||
let active_perp_len = account.active_perp_positions().count();
|
||||
|
||||
let (metas, _health_cu) =
|
||||
context.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?;
|
||||
let fallback_keys = context
|
||||
.derive_fallback_oracle_keys(fallback_config, account_fetcher)
|
||||
.await?;
|
||||
let (metas, _health_cu) = context.derive_health_check_remaining_account_metas(
|
||||
account,
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
fallback_keys,
|
||||
)?;
|
||||
let accounts: anyhow::Result<Vec<KeyedAccountSharedData>> = stream::iter(metas.iter())
|
||||
.then(|meta| async {
|
||||
Ok(KeyedAccountSharedData::new(
|
||||
|
@ -34,9 +44,13 @@ pub async fn new(
|
|||
begin_perp: active_token_len * 2,
|
||||
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
||||
staleness_slot: None,
|
||||
begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts
|
||||
usd_oracle_index: None,
|
||||
sol_oracle_index: None,
|
||||
begin_fallback_oracles: metas.len(),
|
||||
usdc_oracle_index: metas
|
||||
.iter()
|
||||
.position(|m| m.pubkey == pyth_mainnet_usdc_oracle::ID),
|
||||
sol_oracle_index: metas
|
||||
.iter()
|
||||
.position(|m| m.pubkey == pyth_mainnet_sol_oracle::ID),
|
||||
};
|
||||
let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||
|
@ -51,8 +65,13 @@ pub fn new_sync(
|
|||
let active_token_len = account.active_token_positions().count();
|
||||
let active_perp_len = account.active_perp_positions().count();
|
||||
|
||||
let (metas, _health_cu) =
|
||||
context.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?;
|
||||
let (metas, _health_cu) = context.derive_health_check_remaining_account_metas(
|
||||
account,
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
HashMap::new(),
|
||||
)?;
|
||||
let accounts = metas
|
||||
.iter()
|
||||
.map(|meta| {
|
||||
|
@ -70,8 +89,8 @@ pub fn new_sync(
|
|||
begin_perp: active_token_len * 2,
|
||||
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
||||
staleness_slot: None,
|
||||
begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts
|
||||
usd_oracle_index: None,
|
||||
begin_fallback_oracles: metas.len(),
|
||||
usdc_oracle_index: None,
|
||||
sol_oracle_index: None,
|
||||
};
|
||||
let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
pub mod v4;
|
||||
pub mod v6;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{JupiterSwapMode, MangoClient, TransactionBuilder};
|
||||
use crate::{MangoClient, TransactionBuilder};
|
||||
use fixed::types::I80F48;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Version {
|
||||
Mock,
|
||||
V4,
|
||||
V6,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum RawQuote {
|
||||
Mock,
|
||||
V4(v4::QueryRoute),
|
||||
V6(v6::QuoteResponse),
|
||||
}
|
||||
|
||||
|
@ -32,21 +30,6 @@ pub struct Quote {
|
|||
}
|
||||
|
||||
impl Quote {
|
||||
pub fn try_from_v4(
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
route: v4::QueryRoute,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(Quote {
|
||||
input_mint,
|
||||
output_mint,
|
||||
price_impact_pct: route.price_impact_pct,
|
||||
in_amount: route.in_amount.parse()?,
|
||||
out_amount: route.out_amount.parse()?,
|
||||
raw: RawQuote::V4(route),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_from_v6(query: v6::QuoteResponse) -> anyhow::Result<Self> {
|
||||
Ok(Quote {
|
||||
input_mint: Pubkey::from_str(&query.input_mint)?,
|
||||
|
@ -65,7 +48,6 @@ impl Quote {
|
|||
pub fn first_route_label(&self) -> String {
|
||||
let label_maybe = match &self.raw {
|
||||
RawQuote::Mock => Some("mock".into()),
|
||||
RawQuote::V4(raw) => raw.market_infos.first().map(|v| v.label.clone()),
|
||||
RawQuote::V6(raw) => raw
|
||||
.route_plan
|
||||
.first()
|
||||
|
@ -129,21 +111,6 @@ impl<'a> Jupiter<'a> {
|
|||
) -> anyhow::Result<Quote> {
|
||||
Ok(match version {
|
||||
Version::Mock => self.quote_mock(input_mint, output_mint, amount).await?,
|
||||
Version::V4 => Quote::try_from_v4(
|
||||
input_mint,
|
||||
output_mint,
|
||||
self.mango_client
|
||||
.jupiter_v4()
|
||||
.quote(
|
||||
input_mint,
|
||||
output_mint,
|
||||
amount,
|
||||
slippage_bps,
|
||||
JupiterSwapMode::ExactIn,
|
||||
only_direct_routes,
|
||||
)
|
||||
.await?,
|
||||
)?,
|
||||
Version::V6 => Quote::try_from_v6(
|
||||
self.mango_client
|
||||
.jupiter_v6()
|
||||
|
@ -165,12 +132,6 @@ impl<'a> Jupiter<'a> {
|
|||
) -> anyhow::Result<TransactionBuilder> {
|
||||
match "e.raw {
|
||||
RawQuote::Mock => anyhow::bail!("can't prepare jupiter swap for the mock"),
|
||||
RawQuote::V4(raw) => {
|
||||
self.mango_client
|
||||
.jupiter_v4()
|
||||
.prepare_swap_transaction(quote.input_mint, quote.output_mint, raw)
|
||||
.await
|
||||
}
|
||||
RawQuote::V6(raw) => {
|
||||
self.mango_client
|
||||
.jupiter_v6()
|
||||
|
|
|
@ -1,363 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anchor_lang::Id;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
use bincode::Options;
|
||||
|
||||
use crate::{util, TransactionBuilder};
|
||||
use crate::{JupiterSwapMode, MangoClient};
|
||||
|
||||
use anyhow::Context;
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use solana_sdk::signature::Signature;
|
||||
use solana_sdk::{pubkey::Pubkey, signer::Signer};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryResult {
|
||||
pub data: Vec<QueryRoute>,
|
||||
pub time_taken: f64,
|
||||
pub context_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRoute {
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub price_impact_pct: f64,
|
||||
pub market_infos: Vec<QueryMarketInfo>,
|
||||
pub amount: String,
|
||||
pub slippage_bps: u64,
|
||||
pub other_amount_threshold: String,
|
||||
pub swap_mode: String,
|
||||
pub fees: Option<QueryRouteFees>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryMarketInfo {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub input_mint: String,
|
||||
pub output_mint: String,
|
||||
pub not_enough_liquidity: bool,
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub min_in_amount: Option<String>,
|
||||
pub min_out_amount: Option<String>,
|
||||
pub price_impact_pct: Option<f64>,
|
||||
pub lp_fee: QueryFee,
|
||||
pub platform_fee: QueryFee,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryFee {
|
||||
pub amount: String,
|
||||
pub mint: String,
|
||||
pub pct: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRouteFees {
|
||||
pub signature_fee: f64,
|
||||
pub open_orders_deposits: Vec<f64>,
|
||||
pub ata_deposits: Vec<f64>,
|
||||
pub total_fee_and_deposits: f64,
|
||||
#[serde(rename = "minimalSOLForTransaction")]
|
||||
pub minimal_sol_for_transaction: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapRequest {
|
||||
pub route: QueryRoute,
|
||||
pub user_public_key: String,
|
||||
#[serde(rename = "wrapUnwrapSOL")]
|
||||
pub wrap_unwrap_sol: bool,
|
||||
pub compute_unit_price_micro_lamports: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapResponse {
|
||||
pub setup_transaction: Option<String>,
|
||||
pub swap_transaction: String,
|
||||
pub cleanup_transaction: Option<String>,
|
||||
}
|
||||
|
||||
pub struct JupiterV4<'a> {
|
||||
pub mango_client: &'a MangoClient,
|
||||
}
|
||||
|
||||
impl<'a> JupiterV4<'a> {
|
||||
pub async fn quote(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
slippage_bps: u64,
|
||||
swap_mode: JupiterSwapMode,
|
||||
only_direct_routes: bool,
|
||||
) -> anyhow::Result<QueryRoute> {
|
||||
let response = self
|
||||
.mango_client
|
||||
.http_client
|
||||
.get(format!("{}/quote", self.mango_client.client.jupiter_v4_url))
|
||||
.query(&[
|
||||
("inputMint", input_mint.to_string()),
|
||||
("outputMint", output_mint.to_string()),
|
||||
("amount", format!("{}", amount)),
|
||||
("onlyDirectRoutes", only_direct_routes.to_string()),
|
||||
("enforceSingleTx", "true".into()),
|
||||
("filterTopNResult", "10".into()),
|
||||
("slippageBps", format!("{}", slippage_bps)),
|
||||
(
|
||||
"swapMode",
|
||||
match swap_mode {
|
||||
JupiterSwapMode::ExactIn => "ExactIn",
|
||||
JupiterSwapMode::ExactOut => "ExactOut",
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.context("quote request to jupiter")?;
|
||||
let quote: QueryResult = util::http_error_handling(response).await.with_context(|| {
|
||||
format!("error requesting jupiter route between {input_mint} and {output_mint}")
|
||||
})?;
|
||||
|
||||
let route = quote.data.first().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"no route for swap. found {} routes, but none were usable",
|
||||
quote.data.len()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(route.clone())
|
||||
}
|
||||
|
||||
/// Find the instructions and account lookup tables for a jupiter swap through mango
|
||||
///
|
||||
/// It would be nice if we didn't have to pass input_mint/output_mint - the data is
|
||||
/// definitely in QueryRoute - but it's unclear how.
|
||||
pub async fn prepare_swap_transaction(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
route: &QueryRoute,
|
||||
) -> anyhow::Result<TransactionBuilder> {
|
||||
let source_token = self.mango_client.context.token_by_mint(&input_mint)?;
|
||||
let target_token = self.mango_client.context.token_by_mint(&output_mint)?;
|
||||
|
||||
let swap_response = self
|
||||
.mango_client
|
||||
.http_client
|
||||
.post(format!("{}/swap", self.mango_client.client.jupiter_v4_url))
|
||||
.json(&SwapRequest {
|
||||
route: route.clone(),
|
||||
user_public_key: self.mango_client.owner.pubkey().to_string(),
|
||||
wrap_unwrap_sol: false,
|
||||
compute_unit_price_micro_lamports: None, // we already prioritize
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("swap transaction request to jupiter")?;
|
||||
|
||||
let swap: SwapResponse = util::http_error_handling(swap_response)
|
||||
.await
|
||||
.context("error requesting jupiter swap")?;
|
||||
|
||||
if swap.setup_transaction.is_some() || swap.cleanup_transaction.is_some() {
|
||||
anyhow::bail!(
|
||||
"chosen jupiter route requires setup or cleanup transactions, can't execute"
|
||||
);
|
||||
}
|
||||
|
||||
let jup_tx = bincode::options()
|
||||
.with_fixint_encoding()
|
||||
.reject_trailing_bytes()
|
||||
.deserialize::<solana_sdk::transaction::VersionedTransaction>(
|
||||
&base64::decode(&swap.swap_transaction)
|
||||
.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 compute_budget_program = solana_sdk::compute_budget::ID;
|
||||
// these setup instructions should be placed outside of flashloan begin-end
|
||||
let is_setup_ix = |k: Pubkey| -> bool {
|
||||
k == ata_program || k == token_program || k == compute_budget_program
|
||||
};
|
||||
let (jup_ixs, jup_alts) = self
|
||||
.mango_client
|
||||
.deserialize_instructions_and_alts(&jup_tx.message)
|
||||
.await?;
|
||||
let jup_action_ix_begin = jup_ixs
|
||||
.iter()
|
||||
.position(|ix| !is_setup_ix(ix.program_id))
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("jupiter swap response only had setup-like instructions")
|
||||
})?;
|
||||
let jup_action_ix_end = jup_ixs.len()
|
||||
- jup_ixs
|
||||
.iter()
|
||||
.rev()
|
||||
.position(|ix| !is_setup_ix(ix.program_id))
|
||||
.unwrap();
|
||||
|
||||
let bank_ams = [source_token.first_bank(), target_token.first_bank()]
|
||||
.into_iter()
|
||||
.map(util::to_writable_account_meta)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vault_ams = [source_token.first_vault(), target_token.first_vault()]
|
||||
.into_iter()
|
||||
.map(util::to_writable_account_meta)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let owner = self.mango_client.owner();
|
||||
|
||||
let token_ams = [source_token.mint, target_token.mint]
|
||||
.into_iter()
|
||||
.map(|mint| {
|
||||
util::to_writable_account_meta(
|
||||
anchor_spl::associated_token::get_associated_token_address(&owner, &mint),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let source_loan = if route.swap_mode == "ExactIn" {
|
||||
u64::from_str(&route.amount).unwrap()
|
||||
} else if route.swap_mode == "ExactOut" {
|
||||
u64::from_str(&route.other_amount_threshold).unwrap()
|
||||
} else {
|
||||
anyhow::bail!("unknown swap mode: {}", route.swap_mode);
|
||||
};
|
||||
let loan_amounts = vec![source_loan, 0u64];
|
||||
let num_loans: u8 = loan_amounts.len().try_into().unwrap();
|
||||
|
||||
// This relies on the fact that health account banks will be identical to the first_bank above!
|
||||
let (health_ams, _health_cu) = self
|
||||
.mango_client
|
||||
.derive_health_check_remaining_account_metas(
|
||||
vec![source_token.token_index, target_token.token_index],
|
||||
vec![source_token.token_index, target_token.token_index],
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.context("building health accounts")?;
|
||||
|
||||
let mut instructions = Vec::new();
|
||||
|
||||
for ix in &jup_ixs[..jup_action_ix_begin] {
|
||||
instructions.push(ix.clone());
|
||||
}
|
||||
|
||||
// Ensure the source token account is created (jupiter takes care of the output account)
|
||||
instructions.push(
|
||||
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
|
||||
&owner,
|
||||
&owner,
|
||||
&source_token.mint,
|
||||
&Token::id(),
|
||||
),
|
||||
);
|
||||
|
||||
instructions.push(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::FlashLoanBegin {
|
||||
account: self.mango_client.mango_account_address,
|
||||
owner,
|
||||
token_program: Token::id(),
|
||||
instructions: solana_sdk::sysvar::instructions::id(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
ams.extend(bank_ams);
|
||||
ams.extend(vault_ams.clone());
|
||||
ams.extend(token_ams.clone());
|
||||
ams.push(util::to_readonly_account_meta(self.mango_client.group()));
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanBegin {
|
||||
loan_amounts,
|
||||
}),
|
||||
});
|
||||
for ix in &jup_ixs[jup_action_ix_begin..jup_action_ix_end] {
|
||||
instructions.push(ix.clone());
|
||||
}
|
||||
instructions.push(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::FlashLoanEnd {
|
||||
account: self.mango_client.mango_account_address,
|
||||
owner,
|
||||
token_program: Token::id(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
ams.extend(health_ams);
|
||||
ams.extend(vault_ams);
|
||||
ams.extend(token_ams);
|
||||
ams.push(util::to_readonly_account_meta(self.mango_client.group()));
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanEndV2 {
|
||||
num_loans,
|
||||
flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Swap,
|
||||
}),
|
||||
});
|
||||
for ix in &jup_ixs[jup_action_ix_end..] {
|
||||
instructions.push(ix.clone());
|
||||
}
|
||||
|
||||
let mut address_lookup_tables = self.mango_client.mango_address_lookup_tables().await?;
|
||||
address_lookup_tables.extend(jup_alts.into_iter());
|
||||
|
||||
let payer = owner; // maybe use fee_payer? but usually it's the same
|
||||
|
||||
Ok(TransactionBuilder {
|
||||
instructions,
|
||||
address_lookup_tables,
|
||||
payer,
|
||||
signers: vec![self.mango_client.owner.clone()],
|
||||
config: self.mango_client.client.transaction_builder_config,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn swap(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
slippage_bps: u64,
|
||||
swap_mode: JupiterSwapMode,
|
||||
only_direct_routes: bool,
|
||||
) -> anyhow::Result<Signature> {
|
||||
let route = self
|
||||
.quote(
|
||||
input_mint,
|
||||
output_mint,
|
||||
amount,
|
||||
slippage_bps,
|
||||
swap_mode,
|
||||
only_direct_routes,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let tx_builder = self
|
||||
.prepare_swap_transaction(input_mint, output_mint, &route)
|
||||
.await?;
|
||||
|
||||
tx_builder.send_and_confirm(&self.mango_client.client).await
|
||||
}
|
||||
}
|
|
@ -194,15 +194,15 @@ impl<'a> JupiterV6<'a> {
|
|||
),
|
||||
),
|
||||
];
|
||||
let client = &self.mango_client.client;
|
||||
if !client.jupiter_token.is_empty() {
|
||||
query_args.push(("token", client.jupiter_token.clone()));
|
||||
let config = self.mango_client.client.config();
|
||||
if !config.jupiter_token.is_empty() {
|
||||
query_args.push(("token", config.jupiter_token.clone()));
|
||||
}
|
||||
|
||||
let response = self
|
||||
.mango_client
|
||||
.http_client
|
||||
.get(format!("{}/quote", client.jupiter_v6_url))
|
||||
.get(format!("{}/quote", config.jupiter_v6_url))
|
||||
.query(&query_args)
|
||||
.send()
|
||||
.await
|
||||
|
@ -237,6 +237,7 @@ impl<'a> JupiterV6<'a> {
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
let owner = self.mango_client.owner();
|
||||
let account = &self.mango_client.mango_account().await?;
|
||||
|
||||
let token_ams = [source_token.mint, target_token.mint]
|
||||
.into_iter()
|
||||
|
@ -259,6 +260,7 @@ impl<'a> JupiterV6<'a> {
|
|||
let (health_ams, _health_cu) = self
|
||||
.mango_client
|
||||
.derive_health_check_remaining_account_metas(
|
||||
account,
|
||||
vec![source_token.token_index, target_token.token_index],
|
||||
vec![source_token.token_index, target_token.token_index],
|
||||
vec![],
|
||||
|
@ -267,15 +269,15 @@ impl<'a> JupiterV6<'a> {
|
|||
.context("building health accounts")?;
|
||||
|
||||
let mut query_args = vec![];
|
||||
let client = &self.mango_client.client;
|
||||
if !client.jupiter_token.is_empty() {
|
||||
query_args.push(("token", client.jupiter_token.clone()));
|
||||
let config = self.mango_client.client.config();
|
||||
if !config.jupiter_token.is_empty() {
|
||||
query_args.push(("token", config.jupiter_token.clone()));
|
||||
}
|
||||
|
||||
let swap_response = self
|
||||
.mango_client
|
||||
.http_client
|
||||
.post(format!("{}/swap-instructions", client.jupiter_v6_url))
|
||||
.post(format!("{}/swap-instructions", config.jupiter_v6_url))
|
||||
.query(&query_args)
|
||||
.json(&SwapRequest {
|
||||
user_public_key: owner.to_string(),
|
||||
|
@ -386,7 +388,12 @@ impl<'a> JupiterV6<'a> {
|
|||
address_lookup_tables,
|
||||
payer,
|
||||
signers: vec![self.mango_client.owner.clone()],
|
||||
config: self.mango_client.client.transaction_builder_config,
|
||||
config: self
|
||||
.mango_client
|
||||
.client
|
||||
.config()
|
||||
.transaction_builder_config
|
||||
.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -8,12 +8,15 @@ pub mod account_update_stream;
|
|||
pub mod chain_data;
|
||||
mod chain_data_fetcher;
|
||||
mod client;
|
||||
pub mod confirm_transaction;
|
||||
mod context;
|
||||
pub mod error_tracking;
|
||||
pub mod gpa;
|
||||
pub mod health_cache;
|
||||
pub mod jupiter;
|
||||
pub mod perp_pnl;
|
||||
pub mod priority_fees;
|
||||
pub mod priority_fees_cli;
|
||||
pub mod snapshot_source;
|
||||
mod util;
|
||||
pub mod websocket_source;
|
||||
|
|
|
@ -17,6 +17,7 @@ pub enum Direction {
|
|||
/// Note: keep in sync with perp.ts:getSettlePnlCandidates
|
||||
pub async fn fetch_top(
|
||||
context: &crate::context::MangoGroupContext,
|
||||
fallback_config: &FallbackOracleConfig,
|
||||
account_fetcher: &impl AccountFetcher,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
direction: Direction,
|
||||
|
@ -91,9 +92,10 @@ pub async fn fetch_top(
|
|||
} else {
|
||||
I80F48::ZERO
|
||||
};
|
||||
let perp_max_settle = crate::health_cache::new(context, account_fetcher, &acc)
|
||||
.await?
|
||||
.perp_max_settle(perp_market.settle_token_index)?;
|
||||
let perp_max_settle =
|
||||
crate::health_cache::new(context, fallback_config, account_fetcher, &acc)
|
||||
.await?
|
||||
.perp_max_settle(perp_market.settle_token_index)?;
|
||||
let settleable_pnl = if perp_max_settle > 0 {
|
||||
(*pnl).max(-perp_max_settle)
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
use futures::{SinkExt, StreamExt};
|
||||
use jsonrpc_core::{MethodCall, Notification, Params, Version};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio_tungstenite::tungstenite::protocol::Message;
|
||||
use tracing::*;
|
||||
|
||||
pub trait PriorityFeeProvider: Sync + Send {
|
||||
fn compute_unit_fee_microlamports(&self) -> u64;
|
||||
}
|
||||
|
||||
pub struct FixedPriorityFeeProvider {
|
||||
pub compute_unit_fee_microlamports: u64,
|
||||
}
|
||||
|
||||
impl FixedPriorityFeeProvider {
|
||||
pub fn new(fee_microlamports: u64) -> Self {
|
||||
Self {
|
||||
compute_unit_fee_microlamports: fee_microlamports,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PriorityFeeProvider for FixedPriorityFeeProvider {
|
||||
fn compute_unit_fee_microlamports(&self) -> u64 {
|
||||
self.compute_unit_fee_microlamports
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Builder)]
|
||||
pub struct EmaPriorityFeeProviderConfig {
|
||||
pub percentile: u8,
|
||||
|
||||
#[builder(default = "0.2")]
|
||||
pub alpha: f64,
|
||||
|
||||
pub fallback_prio: u64,
|
||||
|
||||
#[builder(default = "Duration::from_secs(15)")]
|
||||
pub max_age: Duration,
|
||||
}
|
||||
|
||||
impl EmaPriorityFeeProviderConfig {
|
||||
pub fn builder() -> EmaPriorityFeeProviderConfigBuilder {
|
||||
EmaPriorityFeeProviderConfigBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CuPercentileEmaPriorityFeeProviderData {
|
||||
ema: f64,
|
||||
last_update: Option<Instant>,
|
||||
}
|
||||
|
||||
pub struct CuPercentileEmaPriorityFeeProvider {
|
||||
data: RwLock<CuPercentileEmaPriorityFeeProviderData>,
|
||||
config: EmaPriorityFeeProviderConfig,
|
||||
}
|
||||
|
||||
impl PriorityFeeProvider for CuPercentileEmaPriorityFeeProvider {
|
||||
fn compute_unit_fee_microlamports(&self) -> u64 {
|
||||
let data = self.data.read().unwrap();
|
||||
if let Some(last_update) = data.last_update {
|
||||
if Instant::now().duration_since(last_update) > self.config.max_age {
|
||||
return self.config.fallback_prio;
|
||||
}
|
||||
} else {
|
||||
return self.config.fallback_prio;
|
||||
}
|
||||
data.ema as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CuPercentileEmaPriorityFeeProvider {
|
||||
pub fn run(
|
||||
config: EmaPriorityFeeProviderConfig,
|
||||
sender: &broadcast::Sender<BlockPrioFees>,
|
||||
) -> (Arc<Self>, JoinHandle<()>) {
|
||||
let this = Arc::new(Self {
|
||||
data: Default::default(),
|
||||
config,
|
||||
});
|
||||
let handle = tokio::spawn({
|
||||
let this_c = this.clone();
|
||||
let rx = sender.subscribe();
|
||||
async move { Self::run_update_job(this_c, rx).await }
|
||||
});
|
||||
(this, handle)
|
||||
}
|
||||
|
||||
async fn run_update_job(provider: Arc<Self>, mut rx: broadcast::Receiver<BlockPrioFees>) {
|
||||
let config = &provider.config;
|
||||
loop {
|
||||
let block_prios = rx.recv().await.unwrap();
|
||||
let prio = match block_prios.by_cu_percentile.get(&config.percentile) {
|
||||
Some(v) => *v as f64,
|
||||
None => {
|
||||
error!("percentile not available: {}", config.percentile);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut data = provider.data.write().unwrap();
|
||||
data.ema = data.ema * (1.0 - config.alpha) + config.alpha * prio;
|
||||
data.last_update = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct BlockPrioFees {
|
||||
pub slot: u64,
|
||||
// prio fee percentile in percent -> prio fee
|
||||
pub percentile: HashMap<u8, u64>,
|
||||
// cu percentile in percent -> median prio fee of the group
|
||||
pub by_cu_percentile: HashMap<u8, u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct BlockPrioritizationFeesNotificationContext {
|
||||
slot: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct BlockPrioritizationFeesNotificationValue {
|
||||
by_tx: Vec<u64>,
|
||||
by_tx_percentiles: Vec<f64>,
|
||||
by_cu: Vec<u64>,
|
||||
by_cu_percentiles: Vec<f64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct BlockPrioritizationFeesNotificationParams {
|
||||
context: BlockPrioritizationFeesNotificationContext,
|
||||
value: BlockPrioritizationFeesNotificationValue,
|
||||
}
|
||||
|
||||
fn as_block_prioritization_fees_notification(
|
||||
notification_str: &str,
|
||||
) -> anyhow::Result<Option<BlockPrioFees>> {
|
||||
let notification: Notification = match serde_json::from_str(¬ification_str) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Ok(None), // not a notification at all
|
||||
};
|
||||
if notification.method != "blockPrioritizationFeesNotification" {
|
||||
return Ok(None);
|
||||
}
|
||||
let map = match notification.params {
|
||||
Params::Map(m) => m,
|
||||
_ => anyhow::bail!("unexpected params, expected map"),
|
||||
};
|
||||
let result = map
|
||||
.get("result")
|
||||
.ok_or(anyhow::anyhow!("missing params.result"))?
|
||||
.clone();
|
||||
|
||||
let mut data = BlockPrioFees::default();
|
||||
let v: BlockPrioritizationFeesNotificationParams = serde_json::from_value(result)?;
|
||||
data.slot = v.context.slot;
|
||||
for (percentile, prio) in v.value.by_tx_percentiles.iter().zip(v.value.by_tx.iter()) {
|
||||
let int_perc: u8 = ((percentile * 100.0) as u64).try_into()?;
|
||||
data.percentile.insert(int_perc, *prio);
|
||||
}
|
||||
for (percentile, prio) in v.value.by_cu_percentiles.iter().zip(v.value.by_cu.iter()) {
|
||||
let int_perc: u8 = ((percentile * 100.0) as u64).try_into()?;
|
||||
data.by_cu_percentile.insert(int_perc, *prio);
|
||||
}
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn connect_and_broadcast(
|
||||
url: &str,
|
||||
sender: &broadcast::Sender<BlockPrioFees>,
|
||||
) -> anyhow::Result<()> {
|
||||
let (ws_stream, _) = connect_async(url).await?;
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
|
||||
// Create a JSON-RPC request
|
||||
let call = MethodCall {
|
||||
jsonrpc: Some(Version::V2),
|
||||
method: "blockPrioritizationFeesSubscribe".to_string(),
|
||||
params: Params::None,
|
||||
id: jsonrpc_core::Id::Num(1),
|
||||
};
|
||||
|
||||
let request = serde_json::to_string(&call).unwrap();
|
||||
write.send(Message::Text(request)).await?;
|
||||
|
||||
loop {
|
||||
let timeout = tokio::time::sleep(Duration::from_secs(20));
|
||||
tokio::select! {
|
||||
message = read.next() => {
|
||||
match message {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
if let Some(block_prio) = as_block_prioritization_fees_notification(&text)? {
|
||||
// Failure might just mean there is no receiver right now
|
||||
let _ = sender.send(block_prio);
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Ping(..))) => {}
|
||||
Some(Ok(Message::Pong(..))) => {}
|
||||
Some(Ok(msg @ _)) => {
|
||||
anyhow::bail!("received a non-text message: {:?}", msg);
|
||||
},
|
||||
Some(Err(e)) => {
|
||||
anyhow::bail!("error receiving message: {}", e);
|
||||
}
|
||||
None => {
|
||||
anyhow::bail!("websocket stream closed");
|
||||
}
|
||||
}
|
||||
},
|
||||
_ = timeout => {
|
||||
anyhow::bail!("timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_and_broadcast_loop(url: &str, sender: broadcast::Sender<BlockPrioFees>) {
|
||||
loop {
|
||||
if let Err(err) = connect_and_broadcast(url, &sender).await {
|
||||
info!("recent block prio feed error, restarting: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_broadcast_from_websocket_feed(
|
||||
url: String,
|
||||
) -> (broadcast::Sender<BlockPrioFees>, JoinHandle<()>) {
|
||||
let (sender, _) = broadcast::channel(10);
|
||||
let sender_c = sender.clone();
|
||||
let handle = tokio::spawn(async move { connect_and_broadcast_loop(&url, sender_c).await });
|
||||
(sender, handle)
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
use std::sync::Arc;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::*;
|
||||
|
||||
use crate::priority_fees::*;
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PriorityFeeStyleArg {
|
||||
None,
|
||||
Fixed,
|
||||
LiteRpcCuPercentileEma,
|
||||
}
|
||||
|
||||
#[derive(clap::Args, Debug, Clone)]
|
||||
pub struct PriorityFeeArgs {
|
||||
/// choose prio fee style
|
||||
#[clap(long, env, value_enum, default_value = "none")]
|
||||
prioritization_style: PriorityFeeStyleArg,
|
||||
|
||||
/// prioritize each transaction with this many microlamports/cu
|
||||
///
|
||||
/// for dynamic prio styles, this is the fallback value
|
||||
#[clap(long, env, default_value = "0")]
|
||||
prioritization_micro_lamports: u64,
|
||||
|
||||
#[clap(long, env, default_value = "50")]
|
||||
prioritization_ema_percentile: u8,
|
||||
|
||||
#[clap(long, env, default_value = "0.2")]
|
||||
prioritization_ema_alpha: f64,
|
||||
}
|
||||
|
||||
impl PriorityFeeArgs {
|
||||
pub fn make_prio_provider(
|
||||
&self,
|
||||
lite_rpc_url: String,
|
||||
) -> anyhow::Result<(Option<Arc<dyn PriorityFeeProvider>>, Vec<JoinHandle<()>>)> {
|
||||
let prio_style;
|
||||
if self.prioritization_micro_lamports > 0
|
||||
&& self.prioritization_style == PriorityFeeStyleArg::None
|
||||
{
|
||||
info!("forcing prioritization-style to fixed, since prioritization-micro-lamports was set");
|
||||
prio_style = PriorityFeeStyleArg::Fixed;
|
||||
} else {
|
||||
prio_style = self.prioritization_style;
|
||||
}
|
||||
|
||||
Ok(match prio_style {
|
||||
PriorityFeeStyleArg::None => (None, vec![]),
|
||||
PriorityFeeStyleArg::Fixed => (
|
||||
Some(Arc::new(FixedPriorityFeeProvider::new(
|
||||
self.prioritization_micro_lamports,
|
||||
))),
|
||||
vec![],
|
||||
),
|
||||
PriorityFeeStyleArg::LiteRpcCuPercentileEma => {
|
||||
if lite_rpc_url.is_empty() {
|
||||
anyhow::bail!("cannot use recent-cu-percentile-ema prioritization style without a lite-rpc url");
|
||||
}
|
||||
let (block_prio_broadcaster, block_prio_job) =
|
||||
run_broadcast_from_websocket_feed(lite_rpc_url);
|
||||
let (prio_fee_provider, prio_fee_provider_job) =
|
||||
CuPercentileEmaPriorityFeeProvider::run(
|
||||
EmaPriorityFeeProviderConfig::builder()
|
||||
.percentile(75)
|
||||
.fallback_prio(self.prioritization_micro_lamports)
|
||||
.alpha(self.prioritization_ema_alpha)
|
||||
.percentile(self.prioritization_ema_percentile)
|
||||
.build()
|
||||
.unwrap(),
|
||||
&block_prio_broadcaster,
|
||||
);
|
||||
(
|
||||
Some(prio_fee_provider),
|
||||
vec![block_prio_job, prio_fee_provider_job],
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,17 +1,8 @@
|
|||
use solana_client::{
|
||||
client_error::Result as ClientResult, rpc_client::RpcClient, rpc_request::RpcError,
|
||||
};
|
||||
use solana_sdk::compute_budget::ComputeBudgetInstruction;
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
use solana_sdk::{
|
||||
clock::Slot, commitment_config::CommitmentConfig, signature::Signature,
|
||||
transaction::uses_durable_nonce,
|
||||
};
|
||||
|
||||
use anchor_lang::prelude::{AccountMeta, Pubkey};
|
||||
use anyhow::Context;
|
||||
use std::{thread, time};
|
||||
|
||||
/// Some Result<> types don't convert to anyhow::Result nicely. Force them through stringification.
|
||||
pub trait AnyhowWrap {
|
||||
|
@ -57,67 +48,6 @@ pub fn delay_interval(period: std::time::Duration) -> tokio::time::Interval {
|
|||
interval
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
|
||||
/// Convenience function used in binaries to set up the fmt tracing_subscriber,
|
||||
/// with cololring enabled only if logging to a terminal and with EnvFilter.
|
||||
pub fn tracing_subscriber_init() {
|
||||
|
|
279
mango_v4.json
279
mango_v4.json
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
<<<<<<< HEAD
|
||||
"version": "0.22.0",
|
||||
=======
|
||||
"version": "0.23.0",
|
||||
>>>>>>> main
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -277,6 +281,12 @@
|
|||
"type": {
|
||||
"option": "u16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeeIntervalOpt",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -631,6 +641,17 @@
|
|||
{
|
||||
"name": "platformLiquidationFee",
|
||||
"type": "f32"
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
},
|
||||
{
|
||||
"name": "disableAssetLiquidation",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "collateralFeePerDay",
|
||||
"type": "f32"
|
||||
>>>>>>> main
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1041,6 +1062,27 @@
|
|||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
},
|
||||
{
|
||||
"name": "disableAssetLiquidationOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeePerDayOpt",
|
||||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "forceWithdrawOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
>>>>>>> main
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3763,6 +3805,63 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenForceWithdraw",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bank",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group",
|
||||
"vault",
|
||||
"oracle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "ownerAtaTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "alternateOwnerTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"Only for the unusual case where the owner_ata account is not owned by account.owner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "perpCreateMarket",
|
||||
"docs": [
|
||||
|
@ -5953,6 +6052,25 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenChargeCollateralFees",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "altSet",
|
||||
"accounts": [
|
||||
|
@ -7373,12 +7491,24 @@
|
|||
"name": "forceClose",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "disableAssetLiquidation",
|
||||
"docs": [
|
||||
"If set to 1, deposits cannot be liquidated when an account is liquidatable.",
|
||||
"That means bankrupt accounts may still have assets of this type deposited."
|
||||
],
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "forceWithdraw",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
6
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -7514,11 +7644,36 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"name": "collectedCollateralFees",
|
||||
"docs": [
|
||||
"Collateral fees that have been collected (in native tokens)",
|
||||
"",
|
||||
"See also collected_fees_native and fees_withdrawn."
|
||||
],
|
||||
"type": {
|
||||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeePerDay",
|
||||
"docs": [
|
||||
"The daily collateral fees rate for fully utilized collateral."
|
||||
],
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
>>>>>>> main
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
<<<<<<< HEAD
|
||||
1920
|
||||
=======
|
||||
1900
|
||||
>>>>>>> main
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -7646,12 +7801,28 @@
|
|||
],
|
||||
"type": "u16"
|
||||
},
|
||||
{
|
||||
"name": "padding2",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeeInterval",
|
||||
"docs": [
|
||||
"Intervals in which collateral fee is applied"
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1812
|
||||
1800
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -7773,12 +7944,27 @@
|
|||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "temporaryDelegate",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "temporaryDelegateExpiry",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "lastCollateralFeeCharge",
|
||||
"docs": [
|
||||
"Time at which the last collateral fee was charged"
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
200
|
||||
152
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -9548,12 +9734,16 @@
|
|||
"name": "temporaryDelegateExpiry",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "lastCollateralFeeCharge",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
160
|
||||
152
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -10474,6 +10664,9 @@
|
|||
},
|
||||
{
|
||||
"name": "Swap"
|
||||
},
|
||||
{
|
||||
"name": "SwapWithoutFee"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -10829,6 +11022,9 @@
|
|||
},
|
||||
{
|
||||
"name": "Serum3PlaceOrderV2"
|
||||
},
|
||||
{
|
||||
"name": "TokenForceWithdraw"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -13746,6 +13942,76 @@
|
|||
"index": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TokenCollateralFeeLog",
|
||||
"fields": [
|
||||
{
|
||||
"name": "mangoGroup",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "mangoAccount",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "tokenIndex",
|
||||
"type": "u16",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "assetUsageFraction",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "fee",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ForceWithdrawLog",
|
||||
"fields": [
|
||||
{
|
||||
"name": "mangoGroup",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "mangoAccount",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "tokenIndex",
|
||||
"type": "u16",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "quantity",
|
||||
"type": "u64",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "toTokenAccount",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
|
@ -14093,6 +14359,11 @@
|
|||
"code": 6068,
|
||||
"name": "MissingFeedForCLMMOracle",
|
||||
"msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)"
|
||||
},
|
||||
{
|
||||
"code": 6069,
|
||||
"name": "TokenAssetLiquidationDisabled",
|
||||
"msg": "the asset does not allow liquidation"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "mango-v4"
|
||||
version = "0.22.0"
|
||||
version = "0.23.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
|
@ -32,7 +32,11 @@ borsh = { version = "0.10.3", features = ["const-generics"] }
|
|||
bytemuck = { version = "^1.7.2", features = ["min_const_generics"] }
|
||||
default-env = "0.1.1"
|
||||
derivative = "2.2.0"
|
||||
fixed = { workspace = true, features = ["serde", "borsh", "debug-assert-in-release"] }
|
||||
fixed = { workspace = true, features = [
|
||||
"serde",
|
||||
"borsh",
|
||||
"debug-assert-in-release",
|
||||
] }
|
||||
num_enum = "0.5.1"
|
||||
pyth-sdk-solana = { workspace = true }
|
||||
serde = "^1.0"
|
||||
|
@ -48,7 +52,9 @@ switchboard-program = "0.2"
|
|||
switchboard-v2 = { package = "switchboard-solana", version = "0.28" }
|
||||
|
||||
|
||||
openbook-v2 = { git = "https://github.com/openbook-dex/openbook-v2.git", features = ["no-entrypoint"] }
|
||||
openbook-v2 = { git = "https://github.com/openbook-dex/openbook-v2.git", features = [
|
||||
"no-entrypoint",
|
||||
] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -56,7 +62,9 @@ solana-sdk = { workspace = true, default-features = false }
|
|||
solana-program-test = { workspace = true }
|
||||
solana-logger = { workspace = true }
|
||||
spl-token = { version = "^3.0.0", features = ["no-entrypoint"] }
|
||||
spl-associated-token-account = { version = "^1.0.3", features = ["no-entrypoint"] }
|
||||
spl-associated-token-account = { version = "^1.0.3", features = [
|
||||
"no-entrypoint",
|
||||
] }
|
||||
bincode = "^1.3.1"
|
||||
log = "0.4.14"
|
||||
env_logger = "0.9.0"
|
||||
|
|
Binary file not shown.
|
@ -92,6 +92,12 @@ pub struct FlashLoanEnd<'info> {
|
|||
#[derive(PartialEq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum FlashLoanType {
|
||||
/// An arbitrary flash loan
|
||||
Unknown,
|
||||
/// A flash loan used for a swap where one token is exchanged for another.
|
||||
///
|
||||
/// Deposits in this type get charged the flash_loan_swap_fee_rate
|
||||
Swap,
|
||||
/// Like Swap, but without the flash_loan_swap_fee_rate
|
||||
SwapWithoutFee,
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ pub use stub_oracle_close::*;
|
|||
pub use stub_oracle_create::*;
|
||||
pub use stub_oracle_set::*;
|
||||
pub use token_add_bank::*;
|
||||
pub use token_charge_collateral_fees::*;
|
||||
pub use token_conditional_swap_cancel::*;
|
||||
pub use token_conditional_swap_create::*;
|
||||
pub use token_conditional_swap_start::*;
|
||||
|
@ -67,6 +68,7 @@ pub use token_deposit::*;
|
|||
pub use token_deregister::*;
|
||||
pub use token_edit::*;
|
||||
pub use token_force_close_borrows_with_token::*;
|
||||
pub use token_force_withdraw::*;
|
||||
pub use token_liq_bankruptcy::*;
|
||||
pub use token_liq_with_token::*;
|
||||
pub use token_register::*;
|
||||
|
@ -135,6 +137,7 @@ mod stub_oracle_close;
|
|||
mod stub_oracle_create;
|
||||
mod stub_oracle_set;
|
||||
mod token_add_bank;
|
||||
mod token_charge_collateral_fees;
|
||||
mod token_conditional_swap_cancel;
|
||||
mod token_conditional_swap_create;
|
||||
mod token_conditional_swap_start;
|
||||
|
@ -143,6 +146,7 @@ mod token_deposit;
|
|||
mod token_deregister;
|
||||
mod token_edit;
|
||||
mod token_force_close_borrows_with_token;
|
||||
mod token_force_withdraw;
|
||||
mod token_liq_bankruptcy;
|
||||
mod token_liq_with_token;
|
||||
mod token_register;
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
use crate::error::MangoError;
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
/// Charges collateral fees on an account
|
||||
#[derive(Accounts)]
|
||||
pub struct TokenChargeCollateralFees<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccountFixed>,
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::associated_token::get_associated_token_address;
|
||||
use anchor_spl::token::Token;
|
||||
use anchor_spl::token::TokenAccount;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TokenForceWithdraw<'info> {
|
||||
#[account(
|
||||
constraint = group.load()?.is_ix_enabled(IxGate::TokenForceWithdraw) @ MangoError::IxIsDisabled,
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen,
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccountFixed>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = vault,
|
||||
has_one = oracle,
|
||||
// the mints of bank/vault/token_accounts are implicitly the same because
|
||||
// spl::token::transfer succeeds between token_account and vault
|
||||
)]
|
||||
pub bank: AccountLoader<'info, Bank>,
|
||||
|
||||
#[account(mut)]
|
||||
pub vault: Box<Account<'info, TokenAccount>>,
|
||||
|
||||
/// CHECK: The oracle can be one of several different account types
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
address = get_associated_token_address(&account.load()?.owner, &vault.mint),
|
||||
// NOTE: the owner may have been changed (before immutable owner was a thing)
|
||||
)]
|
||||
pub owner_ata_token_account: Box<Account<'info, TokenAccount>>,
|
||||
|
||||
/// Only for the unusual case where the owner_ata account is not owned by account.owner
|
||||
#[account(
|
||||
mut,
|
||||
constraint = alternate_owner_token_account.owner == account.load()?.owner,
|
||||
)]
|
||||
pub alternate_owner_token_account: Box<Account<'info, TokenAccount>>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
}
|
|
@ -143,6 +143,8 @@ pub enum MangoError {
|
|||
InvalidFeedForCLMMOracle,
|
||||
#[msg("Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)")]
|
||||
MissingFeedForCLMMOracle,
|
||||
#[msg("the asset does not allow liquidation")]
|
||||
TokenAssetLiquidationDisabled,
|
||||
}
|
||||
|
||||
impl MangoError {
|
||||
|
|
|
@ -59,7 +59,7 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
|
|||
pub begin_serum3: usize,
|
||||
pub staleness_slot: Option<u64>,
|
||||
pub begin_fallback_oracles: usize,
|
||||
pub usd_oracle_index: Option<usize>,
|
||||
pub usdc_oracle_index: Option<usize>,
|
||||
pub sol_oracle_index: Option<usize>,
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
|||
ais.len(), expected_ais,
|
||||
active_token_len, active_token_len, active_perp_len, active_perp_len, active_serum3_len
|
||||
);
|
||||
let usd_oracle_index = ais[..]
|
||||
let usdc_oracle_index = ais[..]
|
||||
.iter()
|
||||
.position(|o| o.key == &pyth_mainnet_usdc_oracle::ID);
|
||||
let sol_oracle_index = ais[..]
|
||||
|
@ -93,7 +93,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
|||
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
||||
staleness_slot: Some(Clock::get()?.slot),
|
||||
begin_fallback_oracles: expected_ais,
|
||||
usd_oracle_index,
|
||||
usdc_oracle_index,
|
||||
sol_oracle_index,
|
||||
})
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
|
|||
OracleAccountInfos {
|
||||
oracle,
|
||||
fallback_opt,
|
||||
usd_opt: self.usd_oracle_index.map(|i| &self.ais[i]),
|
||||
usdc_opt: self.usdc_oracle_index.map(|i| &self.ais[i]),
|
||||
sol_opt: self.sol_oracle_index.map(|i| &self.ais[i]),
|
||||
}
|
||||
}
|
||||
|
@ -324,7 +324,7 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> {
|
|||
OracleAccountInfos {
|
||||
oracle,
|
||||
fallback_opt,
|
||||
usd_opt: self.usd_oracle_index.map(|i| &self.fallback_oracles[i]),
|
||||
usdc_opt: self.usd_oracle_index.map(|i| &self.fallback_oracles[i]),
|
||||
sol_opt: self.sol_oracle_index.map(|i| &self.fallback_oracles[i]),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,6 +175,8 @@ pub struct TokenInfo {
|
|||
/// Includes TokenPosition and free Serum3OpenOrders balances.
|
||||
/// Does not include perp upnl or Serum3 reserved amounts.
|
||||
pub balance_spot: I80F48,
|
||||
|
||||
pub allow_asset_liquidation: bool,
|
||||
}
|
||||
|
||||
/// Temporary value used during health computations
|
||||
|
@ -907,6 +909,7 @@ impl HealthCache {
|
|||
}
|
||||
|
||||
/// Liquidatable spot assets mean: actual token deposits and also a positive effective token balance
|
||||
/// and is available for asset liquidation
|
||||
pub fn has_liq_spot_assets(&self) -> bool {
|
||||
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
|
||||
self.token_infos
|
||||
|
@ -914,11 +917,11 @@ impl HealthCache {
|
|||
.zip(health_token_balances.iter())
|
||||
.any(|(ti, b)| {
|
||||
// need 1 native token to use token_liq_with_token
|
||||
ti.balance_spot >= 1 && b.spot_and_perp >= 1
|
||||
ti.balance_spot >= 1 && b.spot_and_perp >= 1 && ti.allow_asset_liquidation
|
||||
})
|
||||
}
|
||||
|
||||
/// Liquidatable spot borrows mean: actual toen borrows plus a negative effective token balance
|
||||
/// Liquidatable spot borrows mean: actual token borrows plus a negative effective token balance
|
||||
pub fn has_liq_spot_borrows(&self) -> bool {
|
||||
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
|
||||
self.token_infos
|
||||
|
@ -932,7 +935,9 @@ impl HealthCache {
|
|||
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
|
||||
let all_iter = || self.token_infos.iter().zip(health_token_balances.iter());
|
||||
all_iter().any(|(ti, b)| ti.balance_spot < 0 && b.spot_and_perp < 0)
|
||||
&& all_iter().any(|(ti, b)| ti.balance_spot >= 1 && b.spot_and_perp >= 1)
|
||||
&& all_iter().any(|(ti, b)| {
|
||||
ti.balance_spot >= 1 && b.spot_and_perp >= 1 && ti.allow_asset_liquidation
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_serum3_open_orders_funds(&self) -> bool {
|
||||
|
@ -1286,6 +1291,7 @@ fn new_health_cache_impl(
|
|||
init_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price),
|
||||
prices,
|
||||
balance_spot: native,
|
||||
allow_asset_liquidation: bank.allows_asset_liquidation(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -682,6 +682,7 @@ mod tests {
|
|||
init_scaled_liab_weight: I80F48::from_num(1.0 + x),
|
||||
prices: Prices::new_single_price(I80F48::from_num(price)),
|
||||
balance_spot: I80F48::ZERO,
|
||||
allow_asset_liquidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1461,27 +1462,49 @@ mod tests {
|
|||
I80F48::ZERO
|
||||
);
|
||||
|
||||
let find_max_borrow = |c: &HealthCache, ratio: f64| {
|
||||
let max_borrow = c
|
||||
.max_borrow_for_health_ratio(&account, bank0_data, I80F48::from_num(ratio))
|
||||
.unwrap();
|
||||
// compute the health ratio we'd get when executing the trade
|
||||
let actual_ratio = {
|
||||
let mut c = c.clone();
|
||||
c.token_infos[0].balance_spot -= max_borrow;
|
||||
c.health_ratio(HealthType::Init).to_num::<f64>()
|
||||
};
|
||||
// the ratio for borrowing one native token extra
|
||||
let plus_ratio = {
|
||||
let mut c = c.clone();
|
||||
c.token_infos[0].balance_spot -= max_borrow + I80F48::ONE;
|
||||
c.health_ratio(HealthType::Init).to_num::<f64>()
|
||||
};
|
||||
(max_borrow, actual_ratio, plus_ratio)
|
||||
let now_ts = system_epoch_secs();
|
||||
|
||||
let cache_after_borrow = |account: &MangoAccountValue,
|
||||
c: &HealthCache,
|
||||
bank: &Bank,
|
||||
amount: I80F48|
|
||||
-> Result<HealthCache> {
|
||||
let mut position = account.token_position(bank.token_index)?.clone();
|
||||
|
||||
let mut bank = bank.clone();
|
||||
bank.withdraw_with_fee(&mut position, amount, now_ts)?;
|
||||
bank.check_net_borrows(c.token_info(bank.token_index)?.prices.oracle)?;
|
||||
|
||||
let mut resulting_cache = c.clone();
|
||||
resulting_cache.adjust_token_balance(&bank, -amount)?;
|
||||
|
||||
Ok(resulting_cache)
|
||||
};
|
||||
let check_max_borrow = |c: &HealthCache, ratio: f64| -> f64 {
|
||||
|
||||
let find_max_borrow =
|
||||
|account: &MangoAccountValue, c: &HealthCache, ratio: f64, bank: &Bank| {
|
||||
let max_borrow = c
|
||||
.max_borrow_for_health_ratio(account, bank, I80F48::from_num(ratio))
|
||||
.unwrap();
|
||||
// compute the health ratio we'd get when executing the trade
|
||||
let actual_ratio = {
|
||||
let c = cache_after_borrow(account, c, bank, max_borrow).unwrap();
|
||||
c.health_ratio(HealthType::Init).to_num::<f64>()
|
||||
};
|
||||
// the ratio for borrowing one native token extra
|
||||
let plus_ratio = {
|
||||
let c = cache_after_borrow(account, c, bank, max_borrow + I80F48::ONE).unwrap();
|
||||
c.health_ratio(HealthType::Init).to_num::<f64>()
|
||||
};
|
||||
(max_borrow, actual_ratio, plus_ratio)
|
||||
};
|
||||
let check_max_borrow = |account: &MangoAccountValue,
|
||||
c: &HealthCache,
|
||||
ratio: f64,
|
||||
bank: &Bank|
|
||||
-> f64 {
|
||||
let initial_ratio = c.health_ratio(HealthType::Init).to_num::<f64>();
|
||||
let (max_borrow, actual_ratio, plus_ratio) = find_max_borrow(c, ratio);
|
||||
let (max_borrow, actual_ratio, plus_ratio) = find_max_borrow(account, c, ratio, bank);
|
||||
println!(
|
||||
"checking target ratio {ratio}: initial ratio: {initial_ratio}, actual ratio: {actual_ratio}, plus ratio: {plus_ratio}, borrow: {max_borrow}",
|
||||
);
|
||||
|
@ -1496,30 +1519,66 @@ mod tests {
|
|||
{
|
||||
let mut health_cache = health_cache.clone();
|
||||
health_cache.token_infos[0].balance_spot = I80F48::from_num(100.0);
|
||||
assert_eq!(check_max_borrow(&health_cache, 50.0), 100.0);
|
||||
assert_eq!(
|
||||
check_max_borrow(&account, &health_cache, 50.0, bank0_data),
|
||||
100.0
|
||||
);
|
||||
}
|
||||
{
|
||||
let mut health_cache = health_cache.clone();
|
||||
health_cache.token_infos[1].balance_spot = I80F48::from_num(50.0); // price 2, so 2*50*0.8 = 80 health
|
||||
check_max_borrow(&health_cache, 100.0);
|
||||
check_max_borrow(&health_cache, 50.0);
|
||||
check_max_borrow(&health_cache, 0.0);
|
||||
check_max_borrow(&account, &health_cache, 100.0, bank0_data);
|
||||
check_max_borrow(&account, &health_cache, 50.0, bank0_data);
|
||||
check_max_borrow(&account, &health_cache, 0.0, bank0_data);
|
||||
}
|
||||
{
|
||||
let mut health_cache = health_cache.clone();
|
||||
health_cache.token_infos[0].balance_spot = I80F48::from_num(50.0);
|
||||
health_cache.token_infos[1].balance_spot = I80F48::from_num(50.0);
|
||||
check_max_borrow(&health_cache, 100.0);
|
||||
check_max_borrow(&health_cache, 50.0);
|
||||
check_max_borrow(&health_cache, 0.0);
|
||||
check_max_borrow(&account, &health_cache, 100.0, bank0_data);
|
||||
check_max_borrow(&account, &health_cache, 50.0, bank0_data);
|
||||
check_max_borrow(&account, &health_cache, 0.0, bank0_data);
|
||||
}
|
||||
{
|
||||
let mut health_cache = health_cache.clone();
|
||||
health_cache.token_infos[0].balance_spot = I80F48::from_num(-50.0);
|
||||
health_cache.token_infos[1].balance_spot = I80F48::from_num(50.0);
|
||||
check_max_borrow(&health_cache, 100.0);
|
||||
check_max_borrow(&health_cache, 50.0);
|
||||
check_max_borrow(&health_cache, 0.0);
|
||||
check_max_borrow(&account, &health_cache, 100.0, bank0_data);
|
||||
check_max_borrow(&account, &health_cache, 50.0, bank0_data);
|
||||
check_max_borrow(&account, &health_cache, 0.0, bank0_data);
|
||||
}
|
||||
|
||||
// A test that includes init weight scaling
|
||||
{
|
||||
let mut account = account.clone();
|
||||
let mut bank0 = bank0_data.clone();
|
||||
let mut health_cache = health_cache.clone();
|
||||
let tok0_deposits = I80F48::from_num(500.0);
|
||||
health_cache.token_infos[0].balance_spot = tok0_deposits;
|
||||
health_cache.token_infos[1].balance_spot = I80F48::from_num(-100.0); // 2 * 100 * 1.2 = 240 liab
|
||||
|
||||
// This test case needs the bank to know about the deposits
|
||||
let position = account.token_position_mut(bank0.token_index).unwrap().0;
|
||||
bank0.deposit(position, tok0_deposits, now_ts).unwrap();
|
||||
|
||||
// Set up scaling such that token0 health contrib is 500 * 1.0 * 1.0 * (600 / (500 + 300)) = 375
|
||||
bank0.deposit_weight_scale_start_quote = 600.0;
|
||||
bank0.potential_serum_tokens = 300;
|
||||
health_cache.token_infos[0].init_scaled_asset_weight =
|
||||
bank0.scaled_init_asset_weight(I80F48::ONE);
|
||||
|
||||
check_max_borrow(&account, &health_cache, 100.0, &bank0);
|
||||
check_max_borrow(&account, &health_cache, 50.0, &bank0);
|
||||
|
||||
let max_borrow = check_max_borrow(&account, &health_cache, 0.0, &bank0);
|
||||
// that borrow leaves 240 tokens in the account and <600 total in bank
|
||||
assert!((260.0 - max_borrow).abs() < 0.3);
|
||||
|
||||
bank0.deposit_weight_scale_start_quote = 500.0;
|
||||
let max_borrow = check_max_borrow(&account, &health_cache, 0.0, &bank0);
|
||||
// 500 - 222.6 = 277.4 remaining token 0 deposits
|
||||
// 277.4 * 500 / (277.4 + 300) = 240.2 (compensating the -240 liab)
|
||||
assert!((222.6 - max_borrow).abs() < 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -337,7 +337,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
// Create the token position now, so we can compute the pre-health with fixed order health accounts
|
||||
let (_, raw_token_index, _) = account.ensure_token_position(bank.token_index)?;
|
||||
|
||||
// Transfer any excess over the inital balance of the token account back
|
||||
// Transfer any excess over the initial balance of the token account back
|
||||
// into the vault. Compute the total change in the vault balance.
|
||||
let mut change = -I80F48::from(bank.flash_loan_approved_amount);
|
||||
if token_account.amount > bank.flash_loan_token_account_initial {
|
||||
|
@ -378,10 +378,10 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
|
||||
match flash_loan_type {
|
||||
FlashLoanType::Unknown => {}
|
||||
FlashLoanType::Swap => {
|
||||
FlashLoanType::Swap | FlashLoanType::SwapWithoutFee => {
|
||||
require_msg!(
|
||||
changes.len() == 2,
|
||||
"when flash_loan_type is Swap there must be exactly 2 token vault changes"
|
||||
"when flash_loan_type is Swap or SwapWithoutFee there must be exactly 2 token vault changes"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ pub fn group_edit(
|
|||
mngo_token_index_opt: Option<TokenIndex>,
|
||||
buyback_fees_expiry_interval_opt: Option<u64>,
|
||||
allowed_fast_listings_per_interval_opt: Option<u16>,
|
||||
collateral_fee_interval_opt: Option<u64>,
|
||||
) -> Result<()> {
|
||||
let mut group = ctx.accounts.group.load_mut()?;
|
||||
|
||||
|
@ -116,5 +117,14 @@ pub fn group_edit(
|
|||
group.allowed_fast_listings_per_interval = allowed_fast_listings_per_interval;
|
||||
}
|
||||
|
||||
if let Some(collateral_fee_interval) = collateral_fee_interval_opt {
|
||||
msg!(
|
||||
"Collateral fee interval old {:?}, new {:?}",
|
||||
group.collateral_fee_interval,
|
||||
collateral_fee_interval
|
||||
);
|
||||
group.collateral_fee_interval = collateral_fee_interval;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -95,6 +95,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
|
|||
IxGate::TokenConditionalSwapCreateLinearAuction,
|
||||
);
|
||||
log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2);
|
||||
log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw);
|
||||
|
||||
group.ix_gate = ix_gate;
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ pub use stub_oracle_close::*;
|
|||
pub use stub_oracle_create::*;
|
||||
pub use stub_oracle_set::*;
|
||||
pub use token_add_bank::*;
|
||||
pub use token_charge_collateral_fees::*;
|
||||
pub use token_conditional_swap_cancel::*;
|
||||
pub use token_conditional_swap_create::*;
|
||||
pub use token_conditional_swap_start::*;
|
||||
|
@ -58,6 +59,7 @@ pub use token_deposit::*;
|
|||
pub use token_deregister::*;
|
||||
pub use token_edit::*;
|
||||
pub use token_force_close_borrows_with_token::*;
|
||||
pub use token_force_withdraw::*;
|
||||
pub use token_liq_bankruptcy::*;
|
||||
pub use token_liq_with_token::*;
|
||||
pub use token_register::*;
|
||||
|
@ -117,6 +119,7 @@ mod stub_oracle_close;
|
|||
mod stub_oracle_create;
|
||||
mod stub_oracle_set;
|
||||
mod token_add_bank;
|
||||
mod token_charge_collateral_fees;
|
||||
mod token_conditional_swap_cancel;
|
||||
mod token_conditional_swap_create;
|
||||
mod token_conditional_swap_start;
|
||||
|
@ -125,6 +128,7 @@ mod token_deposit;
|
|||
mod token_deregister;
|
||||
mod token_edit;
|
||||
mod token_force_close_borrows_with_token;
|
||||
mod token_force_withdraw;
|
||||
mod token_liq_bankruptcy;
|
||||
mod token_liq_with_token;
|
||||
mod token_register;
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
use crate::accounts_zerocopy::*;
|
||||
use crate::health::*;
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::{emit_stack, TokenBalanceLog, TokenCollateralFeeLog};
|
||||
|
||||
pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
|
||||
if group.collateral_fee_interval == 0 {
|
||||
// By resetting, a new enabling of collateral fees will not immediately create a charge
|
||||
account.fixed.last_collateral_fee_charge = 0;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// When collateral fees are enabled the first time, don't immediately charge
|
||||
if account.fixed.last_collateral_fee_charge == 0 {
|
||||
account.fixed.last_collateral_fee_charge = now_ts;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Is the next fee-charging due?
|
||||
let last_charge_ts = account.fixed.last_collateral_fee_charge;
|
||||
if now_ts < last_charge_ts + group.collateral_fee_interval {
|
||||
return Ok(());
|
||||
}
|
||||
account.fixed.last_collateral_fee_charge = now_ts;
|
||||
|
||||
// Charge the user at most for 2x the interval. So if no one calls this for a long time
|
||||
// there won't be a huge charge based only on the end state.
|
||||
let charge_seconds = (now_ts - last_charge_ts).min(2 * group.collateral_fee_interval);
|
||||
|
||||
// The fees are configured in "interest per day" so we need to get the fraction of days
|
||||
// that has passed since the last update for scaling
|
||||
let inv_seconds_per_day = I80F48::from_num(1.157407407407e-5); // 1 / (24 * 60 * 60)
|
||||
let time_scaling = I80F48::from(charge_seconds) * inv_seconds_per_day;
|
||||
|
||||
let health_cache = {
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
new_health_cache(&account.borrow(), &retriever, now_ts)?
|
||||
};
|
||||
|
||||
// We want to find the total asset health and total liab health, but don't want
|
||||
// to treat borrows that moved into open orders accounts as realized. Hence we
|
||||
// pretend all spot orders are closed and settled and add their funds back to
|
||||
// the token positions.
|
||||
let mut token_balances = health_cache.effective_token_balances(HealthType::Maint);
|
||||
for s3info in health_cache.serum3_infos.iter() {
|
||||
token_balances[s3info.base_info_index].spot_and_perp += s3info.reserved_base;
|
||||
token_balances[s3info.quote_info_index].spot_and_perp += s3info.reserved_quote;
|
||||
}
|
||||
|
||||
let mut total_liab_health = I80F48::ZERO;
|
||||
let mut total_asset_health = I80F48::ZERO;
|
||||
for (info, balance) in health_cache.token_infos.iter().zip(token_balances.iter()) {
|
||||
let health = info.health_contribution(HealthType::Maint, balance.spot_and_perp);
|
||||
if health.is_positive() {
|
||||
total_asset_health += health;
|
||||
} else {
|
||||
total_liab_health -= health;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no assets or no liabs, we can't charge fees
|
||||
if total_asset_health.is_zero() || total_liab_health.is_zero() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Users only pay for assets that are actively used to cover their liabilities.
|
||||
let asset_usage_scaling = (total_liab_health / total_asset_health)
|
||||
.max(I80F48::ZERO)
|
||||
.min(I80F48::ONE);
|
||||
|
||||
let scaling = asset_usage_scaling * time_scaling;
|
||||
|
||||
let token_position_count = account.active_token_positions().count();
|
||||
for bank_ai in &ctx.remaining_accounts[0..token_position_count] {
|
||||
let mut bank = bank_ai.load_mut::<Bank>()?;
|
||||
if bank.collateral_fee_per_day <= 0.0 || bank.maint_asset_weight.is_zero() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (token_position, raw_token_index) = account.token_position_mut(bank.token_index)?;
|
||||
let token_balance = token_position.native(&bank);
|
||||
if token_balance <= 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fee = token_balance * scaling * I80F48::from_num(bank.collateral_fee_per_day);
|
||||
assert!(fee <= token_balance);
|
||||
|
||||
let is_active = bank.withdraw_without_fee(token_position, fee, now_ts)?;
|
||||
if !is_active {
|
||||
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
|
||||
}
|
||||
|
||||
bank.collected_fees_native += fee;
|
||||
bank.collected_collateral_fees += fee;
|
||||
|
||||
let token_info = health_cache.token_info(bank.token_index)?;
|
||||
let token_position = account.token_position(bank.token_index)?;
|
||||
|
||||
emit_stack(TokenCollateralFeeLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: bank.token_index,
|
||||
fee: fee.to_bits(),
|
||||
asset_usage_fraction: asset_usage_scaling.to_bits(),
|
||||
price: token_info.prices.oracle.to_bits(),
|
||||
});
|
||||
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: bank.token_index,
|
||||
indexed_position: token_position.indexed_position.to_bits(),
|
||||
deposit_index: bank.deposit_index.to_bits(),
|
||||
borrow_index: bank.borrow_index.to_bits(),
|
||||
})
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -53,6 +53,9 @@ pub fn token_edit(
|
|||
deposit_limit_opt: Option<u64>,
|
||||
zero_util_rate: Option<f32>,
|
||||
platform_liquidation_fee: Option<f32>,
|
||||
disable_asset_liquidation_opt: Option<bool>,
|
||||
collateral_fee_per_day: Option<f32>,
|
||||
force_withdraw_opt: Option<bool>,
|
||||
) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
|
||||
|
@ -482,6 +485,43 @@ pub fn token_edit(
|
|||
platform_liquidation_fee
|
||||
);
|
||||
bank.platform_liquidation_fee = I80F48::from_num(platform_liquidation_fee);
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
if platform_liquidation_fee != 0.0 {
|
||||
require_group_admin = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(collateral_fee_per_day) = collateral_fee_per_day {
|
||||
msg!(
|
||||
"Collateral fee per day old {:?}, new {:?}",
|
||||
bank.collateral_fee_per_day,
|
||||
collateral_fee_per_day
|
||||
);
|
||||
bank.collateral_fee_per_day = collateral_fee_per_day;
|
||||
if collateral_fee_per_day != 0.0 {
|
||||
require_group_admin = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(disable_asset_liquidation) = disable_asset_liquidation_opt {
|
||||
msg!(
|
||||
"Asset liquidation disabled old {:?}, new {:?}",
|
||||
bank.disable_asset_liquidation,
|
||||
disable_asset_liquidation
|
||||
);
|
||||
bank.disable_asset_liquidation = u8::from(disable_asset_liquidation);
|
||||
require_group_admin = true;
|
||||
}
|
||||
|
||||
if let Some(force_withdraw) = force_withdraw_opt {
|
||||
msg!(
|
||||
"Force withdraw old {:?}, new {:?}",
|
||||
bank.force_withdraw,
|
||||
force_withdraw
|
||||
);
|
||||
bank.force_withdraw = u8::from(force_withdraw);
|
||||
>>>>>>> main
|
||||
require_group_admin = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
use crate::accounts_zerocopy::AccountInfoRef;
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::{emit_stack, ForceWithdrawLog, TokenBalanceLog};
|
||||
|
||||
pub fn token_force_withdraw(ctx: Context<TokenForceWithdraw>) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
let token_index = ctx.accounts.bank.load()?.token_index;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
|
||||
let mut bank = ctx.accounts.bank.load_mut()?;
|
||||
require!(bank.is_force_withdraw(), MangoError::SomeError);
|
||||
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
|
||||
let withdraw_target = if ctx.accounts.owner_ata_token_account.owner == account.fixed.owner {
|
||||
ctx.accounts.owner_ata_token_account.to_account_info()
|
||||
} else {
|
||||
ctx.accounts.alternate_owner_token_account.to_account_info()
|
||||
};
|
||||
|
||||
let (position, raw_token_index) = account.token_position_mut(token_index)?;
|
||||
let native_position = position.native(&bank);
|
||||
|
||||
// Check >= to allow calling this on 0 deposits to close the token position
|
||||
require_gte!(native_position, I80F48::ZERO);
|
||||
let amount = native_position.floor().to_num::<u64>();
|
||||
let amount_i80f48 = I80F48::from(amount);
|
||||
|
||||
// Update the bank and position
|
||||
let position_is_active = bank.withdraw_without_fee(position, amount_i80f48, now_ts)?;
|
||||
|
||||
// Provide a readable error message in case the vault doesn't have enough tokens
|
||||
if ctx.accounts.vault.amount < amount {
|
||||
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
|
||||
format!(
|
||||
"bank vault does not have enough tokens, need {} but have {}",
|
||||
amount, ctx.accounts.vault.amount
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// Transfer the actual tokens
|
||||
let group_seeds = group_seeds!(group);
|
||||
token::transfer(
|
||||
CpiContext::new(
|
||||
ctx.accounts.token_program.to_account_info(),
|
||||
token::Transfer {
|
||||
from: ctx.accounts.vault.to_account_info(),
|
||||
to: withdraw_target.clone(),
|
||||
authority: ctx.accounts.group.to_account_info(),
|
||||
},
|
||||
)
|
||||
.with_signer(&[group_seeds]),
|
||||
amount,
|
||||
)?;
|
||||
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index,
|
||||
indexed_position: position.indexed_position.to_bits(),
|
||||
deposit_index: bank.deposit_index.to_bits(),
|
||||
borrow_index: bank.borrow_index.to_bits(),
|
||||
});
|
||||
|
||||
// Get the oracle price, even if stale or unconfident: We want to allow force withdraws
|
||||
// even if the oracle is bad.
|
||||
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
|
||||
let unsafe_oracle_state = oracle_state_unchecked(
|
||||
&OracleAccountInfos::from_reader(oracle_ref),
|
||||
bank.mint_decimals,
|
||||
)?;
|
||||
|
||||
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
|
||||
let amount_usd = (amount_i80f48 * unsafe_oracle_state.price).to_num::<i64>();
|
||||
account.fixed.net_deposits -= amount_usd;
|
||||
|
||||
if !position_is_active {
|
||||
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
|
||||
}
|
||||
|
||||
emit_stack(ForceWithdrawLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index,
|
||||
quantity: amount,
|
||||
price: unsafe_oracle_state.price.to_bits(),
|
||||
to_token_account: withdraw_target.key(),
|
||||
});
|
||||
|
||||
bank.enforce_borrows_lte_deposits()?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -112,6 +112,10 @@ pub(crate) fn liquidation_action(
|
|||
liqee.token_position_and_raw_index(asset_token_index)?;
|
||||
let liqee_asset_native = liqee_asset_position.native(asset_bank);
|
||||
require_gt!(liqee_asset_native, 0);
|
||||
require!(
|
||||
asset_bank.allows_asset_liquidation(),
|
||||
MangoError::TokenAssetLiquidationDisabled
|
||||
);
|
||||
|
||||
let (liqee_liab_position, liqee_liab_raw_index) =
|
||||
liqee.token_position_and_raw_index(liab_token_index)?;
|
||||
|
|
|
@ -44,6 +44,8 @@ pub fn token_register(
|
|||
deposit_limit: u64,
|
||||
zero_util_rate: f32,
|
||||
platform_liquidation_fee: f32,
|
||||
disable_asset_liquidation: bool,
|
||||
collateral_fee_per_day: f32,
|
||||
) -> Result<()> {
|
||||
// Require token 0 to be in the insurance token
|
||||
if token_index == INSURANCE_TOKEN_INDEX {
|
||||
|
@ -109,6 +111,8 @@ pub fn token_register(
|
|||
deposit_weight_scale_start_quote,
|
||||
reduce_only,
|
||||
force_close: 0,
|
||||
disable_asset_liquidation: u8::from(disable_asset_liquidation),
|
||||
force_withdraw: 0,
|
||||
padding: Default::default(),
|
||||
fees_withdrawn: 0,
|
||||
token_conditional_swap_taker_fee_rate,
|
||||
|
@ -127,7 +131,9 @@ pub fn token_register(
|
|||
zero_util_rate: I80F48::from_num(zero_util_rate),
|
||||
platform_liquidation_fee: I80F48::from_num(platform_liquidation_fee),
|
||||
collected_liquidation_fees: I80F48::ZERO,
|
||||
reserved: [0; 1920],
|
||||
collected_collateral_fees: I80F48::ZERO,
|
||||
collateral_fee_per_day,
|
||||
reserved: [0; 1900],
|
||||
};
|
||||
|
||||
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
|
||||
|
|
|
@ -90,6 +90,8 @@ pub fn token_register_trustless(
|
|||
deposit_weight_scale_start_quote: 5_000_000_000.0, // $5k
|
||||
reduce_only: 2, // deposit-only
|
||||
force_close: 0,
|
||||
disable_asset_liquidation: 1,
|
||||
force_withdraw: 0,
|
||||
padding: Default::default(),
|
||||
fees_withdrawn: 0,
|
||||
token_conditional_swap_taker_fee_rate: 0.0,
|
||||
|
@ -107,7 +109,9 @@ pub fn token_register_trustless(
|
|||
deposit_limit: 0,
|
||||
zero_util_rate: I80F48::ZERO,
|
||||
collected_liquidation_fees: I80F48::ZERO,
|
||||
reserved: [0; 1920],
|
||||
collected_collateral_fees: I80F48::ZERO,
|
||||
collateral_fee_per_day: 0.0, // TODO
|
||||
reserved: [0; 1900],
|
||||
};
|
||||
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
|
||||
if let Ok(oracle_price) = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None)
|
||||
|
|
|
@ -84,6 +84,7 @@ pub mod mango_v4 {
|
|||
mngo_token_index_opt: Option<TokenIndex>,
|
||||
buyback_fees_expiry_interval_opt: Option<u64>,
|
||||
allowed_fast_listings_per_interval_opt: Option<u16>,
|
||||
collateral_fee_interval_opt: Option<u64>,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::group_edit(
|
||||
|
@ -100,6 +101,7 @@ pub mod mango_v4 {
|
|||
mngo_token_index_opt,
|
||||
buyback_fees_expiry_interval_opt,
|
||||
allowed_fast_listings_per_interval_opt,
|
||||
collateral_fee_interval_opt,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -157,6 +159,8 @@ pub mod mango_v4 {
|
|||
deposit_limit: u64,
|
||||
zero_util_rate: f32,
|
||||
platform_liquidation_fee: f32,
|
||||
disable_asset_liquidation: bool,
|
||||
collateral_fee_per_day: f32,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_register(
|
||||
|
@ -190,6 +194,8 @@ pub mod mango_v4 {
|
|||
deposit_limit,
|
||||
zero_util_rate,
|
||||
platform_liquidation_fee,
|
||||
disable_asset_liquidation,
|
||||
collateral_fee_per_day,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -245,6 +251,9 @@ pub mod mango_v4 {
|
|||
deposit_limit_opt: Option<u64>,
|
||||
zero_util_rate_opt: Option<f32>,
|
||||
platform_liquidation_fee_opt: Option<f32>,
|
||||
disable_asset_liquidation_opt: Option<bool>,
|
||||
collateral_fee_per_day_opt: Option<f32>,
|
||||
force_withdraw_opt: Option<bool>,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_edit(
|
||||
|
@ -287,6 +296,9 @@ pub mod mango_v4 {
|
|||
deposit_limit_opt,
|
||||
zero_util_rate_opt,
|
||||
platform_liquidation_fee_opt,
|
||||
disable_asset_liquidation_opt,
|
||||
collateral_fee_per_day_opt,
|
||||
force_withdraw_opt,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -807,6 +819,12 @@ pub mod mango_v4 {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn token_force_withdraw(ctx: Context<TokenForceWithdraw>) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_force_withdraw(ctx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
/// Perps
|
||||
///
|
||||
|
@ -1605,6 +1623,12 @@ pub mod mango_v4 {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_charge_collateral_fees(ctx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn alt_set(ctx: Context<AltSet>, index: u8) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::alt_set(ctx, index)?;
|
||||
|
|
|
@ -795,3 +795,23 @@ pub struct TokenConditionalSwapStartLog {
|
|||
pub incentive_token_index: u16,
|
||||
pub incentive_amount: u64,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct TokenCollateralFeeLog {
|
||||
pub mango_group: Pubkey,
|
||||
pub mango_account: Pubkey,
|
||||
pub token_index: u16,
|
||||
pub asset_usage_fraction: i128,
|
||||
pub fee: i128,
|
||||
pub price: i128,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct ForceWithdrawLog {
|
||||
pub mango_group: Pubkey,
|
||||
pub mango_account: Pubkey,
|
||||
pub token_index: u16,
|
||||
pub quantity: u64,
|
||||
pub price: i128, // I80F48
|
||||
pub to_token_account: Pubkey,
|
||||
}
|
||||
|
|
|
@ -158,8 +158,14 @@ pub struct Bank {
|
|||
pub reduce_only: u8,
|
||||
pub force_close: u8,
|
||||
|
||||
/// If set to 1, deposits cannot be liquidated when an account is liquidatable.
|
||||
/// That means bankrupt accounts may still have assets of this type deposited.
|
||||
pub disable_asset_liquidation: u8,
|
||||
|
||||
pub force_withdraw: u8,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub padding: [u8; 6],
|
||||
pub padding: [u8; 4],
|
||||
|
||||
// Do separate bookkeping for how many tokens were withdrawn
|
||||
// This ensures that collected_fees_native is strictly increasing for stats gathering purposes
|
||||
|
@ -217,8 +223,16 @@ pub struct Bank {
|
|||
/// See also collected_fees_native and fees_withdrawn.
|
||||
pub collected_liquidation_fees: I80F48,
|
||||
|
||||
/// Collateral fees that have been collected (in native tokens)
|
||||
///
|
||||
/// See also collected_fees_native and fees_withdrawn.
|
||||
pub collected_collateral_fees: I80F48,
|
||||
|
||||
/// The daily collateral fees rate for fully utilized collateral.
|
||||
pub collateral_fee_per_day: f32,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub reserved: [u8; 1920],
|
||||
pub reserved: [u8; 1900],
|
||||
}
|
||||
const_assert_eq!(
|
||||
size_of::<Bank>(),
|
||||
|
@ -255,8 +269,9 @@ const_assert_eq!(
|
|||
+ 16 * 3
|
||||
+ 32
|
||||
+ 8
|
||||
+ 16 * 3
|
||||
+ 1920
|
||||
+ 16 * 4
|
||||
+ 4
|
||||
+ 1900
|
||||
);
|
||||
const_assert_eq!(size_of::<Bank>(), 3064);
|
||||
const_assert_eq!(size_of::<Bank>() % 8, 0);
|
||||
|
@ -300,6 +315,7 @@ impl Bank {
|
|||
indexed_borrows: I80F48::ZERO,
|
||||
collected_fees_native: I80F48::ZERO,
|
||||
collected_liquidation_fees: I80F48::ZERO,
|
||||
collected_collateral_fees: I80F48::ZERO,
|
||||
fees_withdrawn: 0,
|
||||
dust: I80F48::ZERO,
|
||||
flash_loan_approved_amount: 0,
|
||||
|
@ -346,7 +362,9 @@ impl Bank {
|
|||
deposit_weight_scale_start_quote: existing_bank.deposit_weight_scale_start_quote,
|
||||
reduce_only: existing_bank.reduce_only,
|
||||
force_close: existing_bank.force_close,
|
||||
padding: [0; 6],
|
||||
disable_asset_liquidation: existing_bank.disable_asset_liquidation,
|
||||
force_withdraw: existing_bank.force_withdraw,
|
||||
padding: [0; 4],
|
||||
token_conditional_swap_taker_fee_rate: existing_bank
|
||||
.token_conditional_swap_taker_fee_rate,
|
||||
token_conditional_swap_maker_fee_rate: existing_bank
|
||||
|
@ -363,7 +381,8 @@ impl Bank {
|
|||
deposit_limit: existing_bank.deposit_limit,
|
||||
zero_util_rate: existing_bank.zero_util_rate,
|
||||
platform_liquidation_fee: existing_bank.platform_liquidation_fee,
|
||||
reserved: [0; 1920],
|
||||
collateral_fee_per_day: existing_bank.collateral_fee_per_day,
|
||||
reserved: [0; 1900],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -375,14 +394,18 @@ impl Bank {
|
|||
require_gte!(self.rate0, I80F48::ZERO);
|
||||
require_gte!(self.rate1, I80F48::ZERO);
|
||||
require_gte!(self.max_rate, I80F48::ZERO);
|
||||
require_gte!(self.adjustment_factor, 0.0);
|
||||
require_gte!(self.loan_fee_rate, 0.0);
|
||||
require_gte!(self.loan_origination_fee_rate, 0.0);
|
||||
require_gte!(self.maint_asset_weight, 0.0);
|
||||
require_gte!(self.stable_price_model.delay_growth_limit, 0.0);
|
||||
require_gte!(self.stable_price_model.stable_growth_limit, 0.0);
|
||||
require_gte!(self.init_asset_weight, 0.0);
|
||||
require_gte!(self.maint_asset_weight, self.init_asset_weight);
|
||||
require_gte!(self.maint_liab_weight, 0.0);
|
||||
require_gte!(self.init_liab_weight, 0.0);
|
||||
require_gte!(self.init_liab_weight, self.maint_liab_weight);
|
||||
require_gte!(self.liquidation_fee, 0.0);
|
||||
require_gte!(self.min_vault_to_deposits_ratio, 0.0);
|
||||
require_gte!(1.0, self.min_vault_to_deposits_ratio);
|
||||
require_gte!(self.net_borrow_limit_per_window_quote, -1);
|
||||
require_gt!(self.borrow_weight_scale_start_quote, 0.0);
|
||||
require_gt!(self.deposit_weight_scale_start_quote, 0.0);
|
||||
|
@ -392,11 +415,22 @@ impl Bank {
|
|||
require_gte!(self.flash_loan_swap_fee_rate, 0.0);
|
||||
require_gte!(self.interest_curve_scaling, 1.0);
|
||||
require_gte!(self.interest_target_utilization, 0.0);
|
||||
require_gte!(1.0, self.interest_target_utilization);
|
||||
require_gte!(self.maint_weight_shift_duration_inv, 0.0);
|
||||
require_gte!(self.maint_weight_shift_asset_target, 0.0);
|
||||
require_gte!(self.maint_weight_shift_liab_target, 0.0);
|
||||
require_gte!(self.zero_util_rate, I80F48::ZERO);
|
||||
require_gte!(self.platform_liquidation_fee, 0.0);
|
||||
if !self.allows_asset_liquidation() {
|
||||
require!(self.are_borrows_reduce_only(), MangoError::SomeError);
|
||||
require_eq!(self.maint_asset_weight, I80F48::ZERO);
|
||||
}
|
||||
require_gte!(self.collateral_fee_per_day, 0.0);
|
||||
if self.is_force_withdraw() {
|
||||
require!(self.are_deposits_reduce_only(), MangoError::SomeError);
|
||||
require!(!self.allows_asset_liquidation(), MangoError::SomeError);
|
||||
require_eq!(self.maint_asset_weight, I80F48::ZERO);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -418,6 +452,14 @@ impl Bank {
|
|||
self.force_close == 1
|
||||
}
|
||||
|
||||
pub fn is_force_withdraw(&self) -> bool {
|
||||
self.force_withdraw == 1
|
||||
}
|
||||
|
||||
pub fn allows_asset_liquidation(&self) -> bool {
|
||||
self.disable_asset_liquidation == 0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn native_borrows(&self) -> I80F48 {
|
||||
self.borrow_index * self.indexed_borrows
|
||||
|
@ -732,7 +774,7 @@ impl Bank {
|
|||
})
|
||||
}
|
||||
|
||||
// withdraw the loan origination fee for a borrow that happenend earlier
|
||||
// withdraw the loan origination fee for a borrow that happened earlier
|
||||
pub fn withdraw_loan_origination_fee(
|
||||
&mut self,
|
||||
position: &mut TokenPosition,
|
||||
|
@ -1052,7 +1094,7 @@ impl Bank {
|
|||
)
|
||||
}
|
||||
|
||||
/// calcualtor function that can be used to compute an interest
|
||||
/// calculator function that can be used to compute an interest
|
||||
/// rate based on the given parameters
|
||||
#[inline(always)]
|
||||
pub fn interest_rate_curve_calculator(
|
||||
|
|
|
@ -98,11 +98,32 @@ pub struct Group {
|
|||
/// Number of fast listings that are allowed per interval
|
||||
pub allowed_fast_listings_per_interval: u16,
|
||||
|
||||
pub reserved: [u8; 1812],
|
||||
pub padding2: [u8; 4],
|
||||
|
||||
/// Intervals in which collateral fee is applied
|
||||
pub collateral_fee_interval: u64,
|
||||
|
||||
pub reserved: [u8; 1800],
|
||||
}
|
||||
const_assert_eq!(
|
||||
size_of::<Group>(),
|
||||
32 + 4 + 32 * 2 + 4 + 32 * 2 + 4 + 4 + 20 * 32 + 32 + 8 + 16 + 32 + 8 + 8 + 2 * 2 + 1812
|
||||
32 + 4
|
||||
+ 32 * 2
|
||||
+ 4
|
||||
+ 32 * 2
|
||||
+ 4
|
||||
+ 4
|
||||
+ 20 * 32
|
||||
+ 32
|
||||
+ 8
|
||||
+ 16
|
||||
+ 32
|
||||
+ 8
|
||||
+ 8
|
||||
+ 2 * 2
|
||||
+ 4
|
||||
+ 8
|
||||
+ 1800
|
||||
);
|
||||
const_assert_eq!(size_of::<Group>(), 2736);
|
||||
const_assert_eq!(size_of::<Group>() % 8, 0);
|
||||
|
@ -224,6 +245,7 @@ pub enum IxGate {
|
|||
TokenConditionalSwapCreatePremiumAuction = 69,
|
||||
TokenConditionalSwapCreateLinearAuction = 70,
|
||||
Serum3PlaceOrderV2 = 71,
|
||||
TokenForceWithdraw = 72,
|
||||
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
|
||||
}
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ impl MangoAccountPdaSeeds {
|
|||
// When not reading via idl, MangoAccount binary data is backwards compatible: when ignoring trailing bytes,
|
||||
// a v2 account can be read as a v1 account and a v3 account can be read as v1 or v2 etc.
|
||||
#[account]
|
||||
#[derive(Derivative)]
|
||||
#[derive(Derivative, PartialEq)]
|
||||
#[derivative(Debug)]
|
||||
pub struct MangoAccount {
|
||||
// fixed
|
||||
|
@ -151,8 +151,14 @@ pub struct MangoAccount {
|
|||
/// Next id to use when adding a token condition swap
|
||||
pub next_token_conditional_swap_id: u64,
|
||||
|
||||
pub temporary_delegate: Pubkey,
|
||||
pub temporary_delegate_expiry: u64,
|
||||
|
||||
/// Time at which the last collateral fee was charged
|
||||
pub last_collateral_fee_charge: u64,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub reserved: [u8; 200],
|
||||
pub reserved: [u8; 152],
|
||||
|
||||
// dynamic
|
||||
pub header_version: u8,
|
||||
|
@ -203,7 +209,10 @@ impl MangoAccount {
|
|||
buyback_fees_accrued_previous: 0,
|
||||
buyback_fees_expiry_timestamp: 0,
|
||||
next_token_conditional_swap_id: 0,
|
||||
reserved: [0; 200],
|
||||
temporary_delegate: Pubkey::default(),
|
||||
temporary_delegate_expiry: 0,
|
||||
last_collateral_fee_charge: 0,
|
||||
reserved: [0; 152],
|
||||
header_version: DEFAULT_MANGO_ACCOUNT_VERSION,
|
||||
padding3: Default::default(),
|
||||
padding4: Default::default(),
|
||||
|
@ -327,11 +336,12 @@ pub struct MangoAccountFixed {
|
|||
pub next_token_conditional_swap_id: u64,
|
||||
pub temporary_delegate: Pubkey,
|
||||
pub temporary_delegate_expiry: u64,
|
||||
pub reserved: [u8; 160],
|
||||
pub last_collateral_fee_charge: u64,
|
||||
pub reserved: [u8; 152],
|
||||
}
|
||||
const_assert_eq!(
|
||||
size_of::<MangoAccountFixed>(),
|
||||
32 * 4 + 8 + 8 * 8 + 32 + 8 + 160
|
||||
32 * 4 + 8 + 8 * 8 + 32 + 8 + 8 + 152
|
||||
);
|
||||
const_assert_eq!(size_of::<MangoAccountFixed>(), 400);
|
||||
const_assert_eq!(size_of::<MangoAccountFixed>() % 8, 0);
|
||||
|
@ -737,6 +747,12 @@ impl<
|
|||
self.dynamic.deref_or_borrow()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn dynamic_reserved_bytes(&self) -> &[u8] {
|
||||
let reserved_offset = self.header().reserved_bytes_offset();
|
||||
&self.dynamic()[reserved_offset..reserved_offset + DYNAMIC_RESERVED_BYTES]
|
||||
}
|
||||
|
||||
/// Returns
|
||||
/// - the position
|
||||
/// - the raw index into the token positions list (for use with get_raw/deactivate)
|
||||
|
@ -1155,6 +1171,7 @@ impl<
|
|||
}
|
||||
}
|
||||
|
||||
// Only used in unit tests
|
||||
pub fn deactivate_perp_position(
|
||||
&mut self,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
|
@ -1196,6 +1213,19 @@ impl<
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_first_active_unused_perp_position(&self) -> Option<&PerpPosition> {
|
||||
let first_unused_position_opt = self.all_perp_positions().find(|p| {
|
||||
p.is_active()
|
||||
&& p.base_position_lots == 0
|
||||
&& p.quote_position_native == 0
|
||||
&& p.bids_base_lots == 0
|
||||
&& p.asks_base_lots == 0
|
||||
&& p.taker_base_lots == 0
|
||||
&& p.taker_quote_lots == 0
|
||||
});
|
||||
first_unused_position_opt
|
||||
}
|
||||
|
||||
pub fn add_perp_order(
|
||||
&mut self,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
|
@ -1852,6 +1882,7 @@ impl<'a, 'info: 'a> MangoAccountLoader<'a> for &'a AccountLoader<'info, MangoAcc
|
|||
mod tests {
|
||||
use bytemuck::Zeroable;
|
||||
use itertools::Itertools;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::state::PostOrderType;
|
||||
|
||||
|
@ -2378,12 +2409,7 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
let reserved_offset = account.header.reserved_bytes_offset();
|
||||
assert!(
|
||||
account.dynamic[reserved_offset..reserved_offset + DYNAMIC_RESERVED_BYTES]
|
||||
.iter()
|
||||
.all(|&v| v == 0)
|
||||
);
|
||||
assert!(account.dynamic_reserved_bytes().iter().all(|&v| v == 0));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -2808,4 +2834,118 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_perp_auto_close_first_unused() {
|
||||
let mut account = make_test_account();
|
||||
|
||||
// Fill all perp slots
|
||||
assert_eq!(account.header.perp_count, 4);
|
||||
account.ensure_perp_position(1, 0).unwrap();
|
||||
account.ensure_perp_position(2, 0).unwrap();
|
||||
account.ensure_perp_position(3, 0).unwrap();
|
||||
account.ensure_perp_position(4, 0).unwrap();
|
||||
assert_eq!(account.active_perp_positions().count(), 4);
|
||||
|
||||
// Force usage of some perp slot (leaves 3 unused)
|
||||
account.perp_position_mut(1).unwrap().taker_base_lots = 10;
|
||||
account.perp_position_mut(2).unwrap().base_position_lots = 10;
|
||||
account.perp_position_mut(4).unwrap().quote_position_native = I80F48::from_num(10);
|
||||
assert!(account.perp_position(3).ok().is_some());
|
||||
|
||||
// Should not succeed anymore
|
||||
{
|
||||
let e = account.ensure_perp_position(5, 0);
|
||||
assert!(e.is_anchor_error_with_code(MangoError::NoFreePerpPositionIndex.error_code()));
|
||||
}
|
||||
|
||||
// Act
|
||||
let to_be_closed_account_opt = account.find_first_active_unused_perp_position();
|
||||
|
||||
assert_eq!(to_be_closed_account_opt.unwrap().market_index, 3)
|
||||
}
|
||||
|
||||
// Attempts reading old mango account data with borsh and with zerocopy
|
||||
#[test]
|
||||
fn test_mango_account_backwards_compatibility() -> Result<()> {
|
||||
use solana_program_test::{find_file, read_file};
|
||||
|
||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
d.push("resources/test");
|
||||
|
||||
// Grab live accounts with
|
||||
// solana account CZGf1qbYPaSoabuA1EmdN8W5UHvH5CeXcNZ7RTx65aVQ --output-file programs/mango-v4/resources/test/mangoaccount-v0.21.3.bin
|
||||
let fixtures = vec!["mangoaccount-v0.21.3"];
|
||||
|
||||
for fixture in fixtures {
|
||||
let filename = format!("resources/test/{}.bin", fixture);
|
||||
let account_bytes = read_file(find_file(&filename).unwrap());
|
||||
|
||||
// Read with borsh
|
||||
let mut account_bytes_slice: &[u8] = &account_bytes;
|
||||
let borsh_account = MangoAccount::try_deserialize(&mut account_bytes_slice)?;
|
||||
|
||||
// Read with zerocopy
|
||||
let zerocopy_reader = MangoAccountValue::from_bytes(&account_bytes[8..])?;
|
||||
let fixed = &zerocopy_reader.fixed;
|
||||
let zerocopy_account = MangoAccount {
|
||||
group: fixed.group,
|
||||
owner: fixed.owner,
|
||||
name: fixed.name,
|
||||
delegate: fixed.delegate,
|
||||
account_num: fixed.account_num,
|
||||
being_liquidated: fixed.being_liquidated,
|
||||
in_health_region: fixed.in_health_region,
|
||||
bump: fixed.bump,
|
||||
padding: Default::default(),
|
||||
net_deposits: fixed.net_deposits,
|
||||
perp_spot_transfers: fixed.perp_spot_transfers,
|
||||
health_region_begin_init_health: fixed.health_region_begin_init_health,
|
||||
frozen_until: fixed.frozen_until,
|
||||
buyback_fees_accrued_current: fixed.buyback_fees_accrued_current,
|
||||
buyback_fees_accrued_previous: fixed.buyback_fees_accrued_previous,
|
||||
buyback_fees_expiry_timestamp: fixed.buyback_fees_expiry_timestamp,
|
||||
next_token_conditional_swap_id: fixed.next_token_conditional_swap_id,
|
||||
temporary_delegate: fixed.temporary_delegate,
|
||||
temporary_delegate_expiry: fixed.temporary_delegate_expiry,
|
||||
last_collateral_fee_charge: fixed.last_collateral_fee_charge,
|
||||
reserved: [0u8; 152],
|
||||
|
||||
header_version: *zerocopy_reader.header_version(),
|
||||
padding3: Default::default(),
|
||||
|
||||
padding4: Default::default(),
|
||||
tokens: zerocopy_reader.all_token_positions().cloned().collect_vec(),
|
||||
|
||||
padding5: Default::default(),
|
||||
serum3: zerocopy_reader.all_serum3_orders().cloned().collect_vec(),
|
||||
|
||||
padding6: Default::default(),
|
||||
perps: zerocopy_reader.all_perp_positions().cloned().collect_vec(),
|
||||
|
||||
padding7: Default::default(),
|
||||
perp_open_orders: zerocopy_reader.all_perp_orders().cloned().collect_vec(),
|
||||
|
||||
padding8: Default::default(),
|
||||
token_conditional_swaps: zerocopy_reader
|
||||
.all_token_conditional_swaps()
|
||||
.cloned()
|
||||
.collect_vec(),
|
||||
|
||||
reserved_dynamic: zerocopy_reader.dynamic_reserved_bytes().try_into().unwrap(),
|
||||
};
|
||||
|
||||
// Both methods agree?
|
||||
assert_eq!(borsh_account, zerocopy_account);
|
||||
|
||||
// Serializing and deserializing produces the same data?
|
||||
let mut borsh_bytes = Vec::new();
|
||||
borsh_account.try_serialize(&mut borsh_bytes)?;
|
||||
let mut slice: &[u8] = &borsh_bytes;
|
||||
let roundtrip_account = MangoAccount::try_deserialize(&mut slice)?;
|
||||
assert_eq!(borsh_account, roundtrip_account);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::state::*;
|
|||
pub const FREE_ORDER_SLOT: PerpMarketIndex = PerpMarketIndex::MAX;
|
||||
|
||||
#[zero_copy]
|
||||
#[derive(AnchorDeserialize, AnchorSerialize, Derivative)]
|
||||
#[derive(AnchorDeserialize, AnchorSerialize, Derivative, PartialEq)]
|
||||
#[derivative(Debug)]
|
||||
pub struct TokenPosition {
|
||||
// TODO: Why did we have deposits and borrows as two different values
|
||||
|
@ -110,7 +110,7 @@ impl TokenPosition {
|
|||
}
|
||||
|
||||
#[zero_copy]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
|
||||
#[derivative(Debug)]
|
||||
pub struct Serum3Orders {
|
||||
pub open_orders: Pubkey,
|
||||
|
@ -203,7 +203,7 @@ impl Default for Serum3Orders {
|
|||
}
|
||||
|
||||
#[zero_copy]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
|
||||
#[derivative(Debug)]
|
||||
pub struct PerpPosition {
|
||||
pub market_index: PerpMarketIndex,
|
||||
|
@ -800,7 +800,7 @@ impl PerpPosition {
|
|||
}
|
||||
|
||||
#[zero_copy]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
|
||||
#[derivative(Debug)]
|
||||
pub struct PerpOpenOrder {
|
||||
pub side_and_tree: u8, // SideAndOrderTree -- enums aren't POD
|
||||
|
|
|
@ -82,7 +82,7 @@ pub mod sol_mint_mainnet {
|
|||
}
|
||||
|
||||
#[zero_copy]
|
||||
#[derive(AnchorDeserialize, AnchorSerialize, Derivative)]
|
||||
#[derive(AnchorDeserialize, AnchorSerialize, Derivative, PartialEq, Eq)]
|
||||
#[derivative(Debug)]
|
||||
pub struct OracleConfig {
|
||||
pub conf_filter: I80F48,
|
||||
|
@ -94,7 +94,7 @@ const_assert_eq!(size_of::<OracleConfig>(), 16 + 8 + 72);
|
|||
const_assert_eq!(size_of::<OracleConfig>(), 96);
|
||||
const_assert_eq!(size_of::<OracleConfig>() % 8, 0);
|
||||
|
||||
#[derive(AnchorDeserialize, AnchorSerialize, Debug)]
|
||||
#[derive(AnchorDeserialize, AnchorSerialize, Debug, Default)]
|
||||
pub struct OracleConfigParams {
|
||||
pub conf_filter: f32,
|
||||
pub max_staleness_slots: Option<u32>,
|
||||
|
@ -278,7 +278,7 @@ fn get_pyth_state(
|
|||
pub struct OracleAccountInfos<'a, T: KeyedAccountReader> {
|
||||
pub oracle: &'a T,
|
||||
pub fallback_opt: Option<&'a T>,
|
||||
pub usd_opt: Option<&'a T>,
|
||||
pub usdc_opt: Option<&'a T>,
|
||||
pub sol_opt: Option<&'a T>,
|
||||
}
|
||||
|
||||
|
@ -287,7 +287,7 @@ impl<'a, T: KeyedAccountReader> OracleAccountInfos<'a, T> {
|
|||
OracleAccountInfos {
|
||||
oracle: acc_reader,
|
||||
fallback_opt: None,
|
||||
usd_opt: None,
|
||||
usdc_opt: None,
|
||||
sol_opt: None,
|
||||
}
|
||||
}
|
||||
|
@ -406,9 +406,7 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
|
|||
OracleType::OrcaCLMM => {
|
||||
let whirlpool = load_whirlpool_state(oracle_info)?;
|
||||
|
||||
let inverted = whirlpool.token_mint_a == usdc_mint_mainnet::ID
|
||||
|| (whirlpool.token_mint_a == sol_mint_mainnet::ID
|
||||
&& whirlpool.token_mint_b != usdc_mint_mainnet::ID);
|
||||
let inverted = whirlpool.is_inverted();
|
||||
let quote_state = if inverted {
|
||||
quote_state_unchecked(acc_infos, &whirlpool.token_mint_a)?
|
||||
} else {
|
||||
|
@ -441,7 +439,7 @@ fn quote_state_unchecked<T: KeyedAccountReader>(
|
|||
) -> Result<OracleState> {
|
||||
if quote_mint == &usdc_mint_mainnet::ID {
|
||||
let usd_feed = acc_infos
|
||||
.usd_opt
|
||||
.usdc_opt
|
||||
.ok_or_else(|| error!(MangoError::MissingFeedForCLMMOracle))?;
|
||||
let usd_state = get_pyth_state(usd_feed, QUOTE_DECIMALS as u8)?;
|
||||
return Ok(usd_state);
|
||||
|
@ -590,13 +588,13 @@ mod tests {
|
|||
let usdc_ais = OracleAccountInfos {
|
||||
oracle: usdc_ai,
|
||||
fallback_opt: None,
|
||||
usd_opt: None,
|
||||
usdc_opt: None,
|
||||
sol_opt: None,
|
||||
};
|
||||
let orca_ais = OracleAccountInfos {
|
||||
oracle: ai,
|
||||
fallback_opt: None,
|
||||
usd_opt: Some(usdc_ai),
|
||||
usdc_opt: Some(usdc_ai),
|
||||
sol_opt: None,
|
||||
};
|
||||
let usdc = oracle_state_unchecked(&usdc_ais, usdc_decimals).unwrap();
|
||||
|
@ -635,7 +633,7 @@ mod tests {
|
|||
let oracle_infos = OracleAccountInfos {
|
||||
oracle: ai,
|
||||
fallback_opt: None,
|
||||
usd_opt: None,
|
||||
usdc_opt: None,
|
||||
sol_opt: None,
|
||||
};
|
||||
assert!(oracle_state_unchecked(&oracle_infos, base_decimals)
|
||||
|
|
|
@ -3,6 +3,10 @@ use solana_program::pubkey::Pubkey;
|
|||
|
||||
use crate::{accounts_zerocopy::KeyedAccountReader, error::MangoError};
|
||||
|
||||
use super::{
|
||||
pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, sol_mint_mainnet, usdc_mint_mainnet,
|
||||
};
|
||||
|
||||
pub mod orca_mainnet_whirlpool {
|
||||
use solana_program::declare_id;
|
||||
declare_id!("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc");
|
||||
|
@ -18,6 +22,30 @@ pub struct WhirlpoolState {
|
|||
pub token_mint_b: Pubkey, // 32
|
||||
}
|
||||
|
||||
impl WhirlpoolState {
|
||||
pub fn is_inverted(&self) -> bool {
|
||||
self.token_mint_a == usdc_mint_mainnet::ID
|
||||
|| (self.token_mint_a == sol_mint_mainnet::ID
|
||||
&& self.token_mint_b != usdc_mint_mainnet::ID)
|
||||
}
|
||||
|
||||
pub fn get_quote_oracle(&self) -> Result<Pubkey> {
|
||||
let mint = if self.is_inverted() {
|
||||
self.token_mint_a
|
||||
} else {
|
||||
self.token_mint_b
|
||||
};
|
||||
|
||||
if mint == usdc_mint_mainnet::ID {
|
||||
return Ok(pyth_mainnet_usdc_oracle::ID);
|
||||
} else if mint == sol_mint_mainnet::ID {
|
||||
return Ok(pyth_mainnet_sol_oracle::ID);
|
||||
} else {
|
||||
return Err(MangoError::MissingFeedForCLMMOracle.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_whirlpool_state(acc_info: &impl KeyedAccountReader) -> Result<WhirlpoolState> {
|
||||
let data = &acc_info.data();
|
||||
require!(
|
||||
|
|
|
@ -45,7 +45,7 @@ pub enum TokenConditionalSwapType {
|
|||
}
|
||||
|
||||
#[zero_copy]
|
||||
#[derive(AnchorDeserialize, AnchorSerialize, Derivative)]
|
||||
#[derive(AnchorDeserialize, AnchorSerialize, Derivative, PartialEq)]
|
||||
#[derivative(Debug)]
|
||||
pub struct TokenConditionalSwap {
|
||||
pub id: u64,
|
||||
|
|
|
@ -17,6 +17,7 @@ mod test_bankrupt_tokens;
|
|||
mod test_basic;
|
||||
mod test_benchmark;
|
||||
mod test_borrow_limits;
|
||||
mod test_collateral_fees;
|
||||
mod test_delegate;
|
||||
mod test_fees_buyback_with_mngo;
|
||||
mod test_force_close;
|
||||
|
|
|
@ -462,6 +462,8 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
|
|||
mint: mints[0].pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
init_asset_weight_opt: Some(0.0),
|
||||
init_liab_weight_opt: Some(2.0),
|
||||
maint_weight_shift_start_opt: Some(start_time + 1000),
|
||||
maint_weight_shift_end_opt: Some(start_time + 2000),
|
||||
maint_weight_shift_asset_target_opt: Some(0.5),
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
#![allow(unused_assignments)]
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_collateral_fees() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..mango_setup::GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
// fund the vaults to allow borrowing
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
mints,
|
||||
1_000_000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
&mints[0..1],
|
||||
1_500, // maint: 0.8 * 1500 = 1200
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
let empty_account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
2,
|
||||
&context.users[1],
|
||||
&mints[0..0],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
let hour = 60 * 60;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
GroupEdit {
|
||||
group,
|
||||
admin,
|
||||
options: mango_v4::instruction::GroupEdit {
|
||||
collateral_fee_interval_opt: Some(6 * hour),
|
||||
..group_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
collateral_fee_per_day_opt: Some(0.1),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
loan_origination_fee_rate_opt: Some(0.0),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: It works on empty accounts
|
||||
//
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenChargeCollateralFeesInstruction {
|
||||
account: empty_account,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut last_time = solana.clock_timestamp().await;
|
||||
solana.set_clock_timestamp(last_time + 9 * hour).await;
|
||||
|
||||
// send it twice, because the first time will never charge anything
|
||||
send_tx(
|
||||
solana,
|
||||
TokenChargeCollateralFeesInstruction {
|
||||
account: empty_account,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
last_time = solana.clock_timestamp().await;
|
||||
|
||||
//
|
||||
// TEST: Without borrows, charging collateral fees has no effect
|
||||
//
|
||||
|
||||
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
|
||||
.await
|
||||
.unwrap();
|
||||
last_time = solana.clock_timestamp().await;
|
||||
solana.set_clock_timestamp(last_time + 9 * hour).await;
|
||||
|
||||
// send it twice, because the first time will never charge anything
|
||||
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
|
||||
.await
|
||||
.unwrap();
|
||||
last_time = solana.clock_timestamp().await;
|
||||
|
||||
// no effect
|
||||
assert_eq!(
|
||||
account_position(solana, account, tokens[0].bank).await,
|
||||
1_500
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: With borrows, there's an effect depending on the time that has passed
|
||||
//
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 500, // maint: -1.2 * 500 = -600 (half of 1200)
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: context.users[1].token_accounts[1],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
solana.set_clock_timestamp(last_time + 9 * hour).await;
|
||||
|
||||
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
|
||||
.await
|
||||
.unwrap();
|
||||
last_time = solana.clock_timestamp().await;
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_position_f64(solana, account, tokens[0].bank).await,
|
||||
1500.0 * (1.0 - 0.1 * (9.0 / 24.0) * (600.0 / 1200.0)),
|
||||
0.01
|
||||
));
|
||||
let last_balance = account_position_f64(solana, account, tokens[0].bank).await;
|
||||
|
||||
//
|
||||
// TEST: More borrows
|
||||
//
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 100, // maint: -1.2 * 600 = -720
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: context.users[1].token_accounts[1],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
solana.set_clock_timestamp(last_time + 7 * hour).await;
|
||||
|
||||
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
|
||||
.await
|
||||
.unwrap();
|
||||
//last_time = solana.clock_timestamp().await;
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_position_f64(solana, account, tokens[0].bank).await,
|
||||
last_balance * (1.0 - 0.1 * (7.0 / 24.0) * (720.0 / (last_balance * 0.8))),
|
||||
0.01
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -438,3 +438,114 @@ async fn test_force_close_perp() -> Result<(), TransportError> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_force_withdraw_token() -> Result<(), TransportError> {
|
||||
let test_builder = TestContextBuilder::new();
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..1];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let token = &tokens[0];
|
||||
|
||||
let deposit_amount = 100;
|
||||
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[0],
|
||||
mints,
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// TEST: fails when force withdraw isn't enabled
|
||||
//
|
||||
assert!(send_tx(
|
||||
solana,
|
||||
TokenForceWithdrawInstruction {
|
||||
account,
|
||||
bank: token.bank,
|
||||
target: context.users[0].token_accounts[0],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// set force withdraw to enabled
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
admin,
|
||||
group,
|
||||
mint: token.mint.pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
init_asset_weight_opt: Some(0.0),
|
||||
maint_asset_weight_opt: Some(0.0),
|
||||
reduce_only_opt: Some(1),
|
||||
disable_asset_liquidation_opt: Some(true),
|
||||
force_withdraw_opt: Some(true),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: can't withdraw to foreign address
|
||||
//
|
||||
assert!(send_tx(
|
||||
solana,
|
||||
TokenForceWithdrawInstruction {
|
||||
account,
|
||||
bank: token.bank,
|
||||
target: context.users[1].token_accounts[0], // bad address/owner
|
||||
},
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
//
|
||||
// TEST: passes and withdraws tokens
|
||||
//
|
||||
let token_account = context.users[0].token_accounts[0];
|
||||
let before_balance = solana.token_account_balance(token_account).await;
|
||||
send_tx(
|
||||
solana,
|
||||
TokenForceWithdrawInstruction {
|
||||
account,
|
||||
bank: token.bank,
|
||||
target: token_account,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let after_balance = solana.token_account_balance(token_account).await;
|
||||
assert_eq!(after_balance, before_balance + deposit_amount);
|
||||
assert!(account_position_closed(solana, account, token.bank).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -335,7 +335,7 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr
|
|||
println!("average success increase: {avg_success_increase}");
|
||||
println!("average failure increase: {avg_failure_increase}");
|
||||
assert!(avg_success_increase < 2_050);
|
||||
assert!(avg_success_increase < 18_500);
|
||||
assert!(avg_failure_increase < 19_500);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -324,6 +324,66 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
//
|
||||
set_bank_stub_oracle_price(solana, group, borrow_token1, admin, 2.0).await;
|
||||
|
||||
//
|
||||
// TEST: can't liquidate if token has no asset weight
|
||||
//
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: collateral_token2.mint.pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
maint_asset_weight_opt: Some(0.0),
|
||||
init_asset_weight_opt: Some(0.0),
|
||||
disable_asset_liquidation_opt: Some(true),
|
||||
reduce_only_opt: Some(1),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let res = send_tx(
|
||||
solana,
|
||||
TokenLiqWithTokenInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
asset_token_index: collateral_token2.index,
|
||||
liab_token_index: borrow_token2.index,
|
||||
asset_bank_index: 0,
|
||||
liab_bank_index: 0,
|
||||
max_liab_transfer: I80F48::from_num(10000.0),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::TokenAssetLiquidationDisabled.into(),
|
||||
"liquidation disabled".to_string(),
|
||||
);
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: collateral_token2.mint.pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
maint_asset_weight_opt: Some(0.8),
|
||||
init_asset_weight_opt: Some(0.6),
|
||||
disable_asset_liquidation_opt: Some(false),
|
||||
reduce_only_opt: Some(0),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: liquidate borrow2 against too little collateral2
|
||||
//
|
||||
|
|
|
@ -1077,6 +1077,8 @@ impl ClientInstruction for TokenRegisterInstruction {
|
|||
deposit_limit: 0,
|
||||
zero_util_rate: 0.0,
|
||||
platform_liquidation_fee: self.platform_liquidation_fee,
|
||||
disable_asset_liquidation: false,
|
||||
collateral_fee_per_day: 0.0,
|
||||
};
|
||||
|
||||
let bank = Pubkey::find_program_address(
|
||||
|
@ -1324,6 +1326,9 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
|
|||
deposit_limit_opt: None,
|
||||
zero_util_rate_opt: None,
|
||||
platform_liquidation_fee_opt: None,
|
||||
disable_asset_liquidation_opt: None,
|
||||
collateral_fee_per_day_opt: None,
|
||||
force_withdraw_opt: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1842,6 +1847,7 @@ pub fn group_edit_instruction_default() -> mango_v4::instruction::GroupEdit {
|
|||
mngo_token_index_opt: None,
|
||||
buyback_fees_expiry_interval_opt: None,
|
||||
allowed_fast_listings_per_interval_opt: None,
|
||||
collateral_fee_interval_opt: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3107,6 +3113,58 @@ impl ClientInstruction for TokenForceCloseBorrowsWithTokenInstruction {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct TokenForceWithdrawInstruction {
|
||||
pub account: Pubkey,
|
||||
pub bank: Pubkey,
|
||||
pub target: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for TokenForceWithdrawInstruction {
|
||||
type Accounts = mango_v4::accounts::TokenForceWithdraw;
|
||||
type Instruction = mango_v4::instruction::TokenForceWithdraw;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {};
|
||||
|
||||
let account = account_loader
|
||||
.load_mango_account(&self.account)
|
||||
.await
|
||||
.unwrap();
|
||||
let bank = account_loader.load::<Bank>(&self.bank).await.unwrap();
|
||||
let health_check_metas = derive_health_check_remaining_account_metas(
|
||||
&account_loader,
|
||||
&account,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: account.fixed.group,
|
||||
account: self.account,
|
||||
bank: self.bank,
|
||||
vault: bank.vault,
|
||||
oracle: bank.oracle,
|
||||
owner_ata_token_account: self.target,
|
||||
alternate_owner_token_account: self.target,
|
||||
token_program: Token::id(),
|
||||
};
|
||||
|
||||
let mut instruction = make_instruction(program_id, &accounts, &instruction);
|
||||
instruction.accounts.extend(health_check_metas.into_iter());
|
||||
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<TestKeypair> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TokenLiqWithTokenInstruction {
|
||||
pub liqee: Pubkey,
|
||||
pub liqor: Pubkey,
|
||||
|
@ -5036,3 +5094,48 @@ impl ClientInstruction for TokenConditionalSwapStartInstruction {
|
|||
vec![self.liqor_owner]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TokenChargeCollateralFeesInstruction {
|
||||
pub account: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for TokenChargeCollateralFeesInstruction {
|
||||
type Accounts = mango_v4::accounts::TokenChargeCollateralFees;
|
||||
type Instruction = mango_v4::instruction::TokenChargeCollateralFees;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
|
||||
let account = account_loader
|
||||
.load_mango_account(&self.account)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let instruction = Self::Instruction {};
|
||||
|
||||
let health_check_metas = derive_health_check_remaining_account_metas(
|
||||
&account_loader,
|
||||
&account,
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: account.fixed.group,
|
||||
account: self.account,
|
||||
};
|
||||
|
||||
let mut instruction = make_instruction(program_id, &accounts, &instruction);
|
||||
instruction.accounts.extend(health_check_metas.into_iter());
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<TestKeypair> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ async function main(): Promise<void> {
|
|||
group,
|
||||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
PublicKey.default,
|
||||
0, // tokenIndex
|
||||
'USDC',
|
||||
{
|
||||
|
@ -101,6 +102,7 @@ async function main(): Promise<void> {
|
|||
group,
|
||||
solDevnetMint,
|
||||
solDevnetOracle,
|
||||
PublicKey.default,
|
||||
4, // tokenIndex
|
||||
'SOL',
|
||||
{
|
||||
|
@ -130,6 +132,7 @@ async function main(): Promise<void> {
|
|||
group,
|
||||
usdtDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
PublicKey.default,
|
||||
5, // tokenIndex
|
||||
'USDT',
|
||||
{
|
||||
|
|
|
@ -94,6 +94,7 @@ async function main() {
|
|||
group,
|
||||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
PublicKey.default,
|
||||
0, // tokenIndex
|
||||
'USDC',
|
||||
{
|
||||
|
@ -124,6 +125,7 @@ async function main() {
|
|||
group,
|
||||
solDevnetMint,
|
||||
solDevnetOracle,
|
||||
PublicKey.default,
|
||||
1, // tokenIndex
|
||||
'SOL',
|
||||
{
|
||||
|
|
|
@ -206,6 +206,7 @@ async function registerTokens() {
|
|||
group,
|
||||
usdcMainnetMint,
|
||||
usdcMainnetOracle.publicKey,
|
||||
PublicKey.default,
|
||||
0,
|
||||
'USDC',
|
||||
{
|
||||
|
@ -226,6 +227,7 @@ async function registerTokens() {
|
|||
group,
|
||||
usdtMainnetMint,
|
||||
usdtMainnetOracle,
|
||||
PublicKey.default,
|
||||
1,
|
||||
'USDT',
|
||||
{
|
||||
|
@ -246,6 +248,7 @@ async function registerTokens() {
|
|||
group,
|
||||
daiMainnetMint,
|
||||
daiMainnetOracle,
|
||||
PublicKey.default,
|
||||
2,
|
||||
'DAI',
|
||||
{
|
||||
|
@ -266,6 +269,7 @@ async function registerTokens() {
|
|||
group,
|
||||
ethMainnetMint,
|
||||
ethMainnetOracle,
|
||||
PublicKey.default,
|
||||
3,
|
||||
'ETH',
|
||||
{
|
||||
|
@ -286,6 +290,7 @@ async function registerTokens() {
|
|||
group,
|
||||
solMainnetMint,
|
||||
solMainnetOracle,
|
||||
PublicKey.default,
|
||||
4,
|
||||
'SOL',
|
||||
{
|
||||
|
@ -306,6 +311,7 @@ async function registerTokens() {
|
|||
group,
|
||||
msolMainnetMint,
|
||||
msolMainnetOracle,
|
||||
PublicKey.default,
|
||||
5,
|
||||
'MSOL',
|
||||
{
|
||||
|
|
|
@ -60,31 +60,31 @@ async function buildClient(): Promise<MangoClient> {
|
|||
);
|
||||
}
|
||||
|
||||
async function groupEdit(): Promise<void> {
|
||||
const client = await buildClient();
|
||||
const group = await client.getGroup(new PublicKey(GROUP_PK));
|
||||
const ix = await client.program.methods
|
||||
.groupEdit(
|
||||
null, // admin
|
||||
null, // fastListingAdmin
|
||||
null, // securityAdmin
|
||||
null, // testing
|
||||
null, // version
|
||||
null, // depositLimitQuote
|
||||
null, // feesPayWithMngo
|
||||
null, // feesMngoBonusRate
|
||||
null, // feesSwapMangoAccount
|
||||
6, // feesMngoTokenIndex
|
||||
null, // feesExpiryInterval
|
||||
5, // allowedFastListingsPerInterval
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: group.admin,
|
||||
})
|
||||
.instruction();
|
||||
console.log(serializeInstructionToBase64(ix));
|
||||
}
|
||||
// async function groupEdit(): Promise<void> {
|
||||
// const client = await buildClient();
|
||||
// const group = await client.getGroup(new PublicKey(GROUP_PK));
|
||||
// const ix = await client.program.methods
|
||||
// .groupEdit(
|
||||
// null, // admin
|
||||
// null, // fastListingAdmin
|
||||
// null, // securityAdmin
|
||||
// null, // testing
|
||||
// null, // version
|
||||
// null, // depositLimitQuote
|
||||
// null, // feesPayWithMngo
|
||||
// null, // feesMngoBonusRate
|
||||
// null, // feesSwapMangoAccount
|
||||
// 6, // feesMngoTokenIndex
|
||||
// null, // feesExpiryInterval
|
||||
// 5, // allowedFastListingsPerInterval
|
||||
// )
|
||||
// .accounts({
|
||||
// group: group.publicKey,
|
||||
// admin: group.admin,
|
||||
// })
|
||||
// .instruction();
|
||||
// console.log(serializeInstructionToBase64(ix));
|
||||
// }
|
||||
|
||||
// async function tokenRegister(): Promise<void> {
|
||||
// const client = await buildClient();
|
||||
|
@ -468,7 +468,7 @@ async function idlSetAuthority(): Promise<void> {
|
|||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
await groupEdit();
|
||||
// await groupEdit();
|
||||
// await tokenRegister();
|
||||
// await tokenEdit();
|
||||
// await perpCreate();
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
||||
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { TokenIndex } from '../src/accounts/bank';
|
||||
import { MangoClient } from '../src/client';
|
||||
import { MANGO_V4_ID } from '../src/constants';
|
||||
|
||||
const CLUSTER: Cluster =
|
||||
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
|
||||
const CLUSTER_URL =
|
||||
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
|
||||
const USER_KEYPAIR =
|
||||
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||
const GROUP_PK =
|
||||
process.env.GROUP_PK || '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
|
||||
const TOKEN_INDEX = Number(process.env.TOKEN_INDEX) as TokenIndex;
|
||||
|
||||
async function forceWithdrawTokens(): Promise<void> {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(CLUSTER_URL!, options);
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
process.env.KEYPAIR || fs.readFileSync(USER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
CLUSTER,
|
||||
MANGO_V4_ID[CLUSTER],
|
||||
{
|
||||
idsSource: 'get-program-accounts',
|
||||
},
|
||||
);
|
||||
|
||||
const group = await client.getGroup(new PublicKey(GROUP_PK));
|
||||
const forceWithdrawBank = group.getFirstBankByTokenIndex(TOKEN_INDEX);
|
||||
if (forceWithdrawBank.reduceOnly != 2) {
|
||||
throw new Error(
|
||||
`Unexpected reduce only state ${forceWithdrawBank.reduceOnly}`,
|
||||
);
|
||||
}
|
||||
if (!forceWithdrawBank.forceWithdraw) {
|
||||
throw new Error(
|
||||
`Unexpected force withdraw state ${forceWithdrawBank.forceWithdraw}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get all mango accounts with deposits for given token
|
||||
const mangoAccountsWithDeposits = (
|
||||
await client.getAllMangoAccounts(group)
|
||||
).filter((a) => a.getTokenBalanceUi(forceWithdrawBank) > 0);
|
||||
|
||||
for (const mangoAccount of mangoAccountsWithDeposits) {
|
||||
const sig = await client.tokenForceWithdraw(
|
||||
group,
|
||||
mangoAccount,
|
||||
TOKEN_INDEX,
|
||||
);
|
||||
console.log(
|
||||
` tokenForceWithdraw for ${mangoAccount.publicKey}, owner ${
|
||||
mangoAccount.owner
|
||||
}, sig https://explorer.solana.com/tx/${sig}?cluster=${
|
||||
CLUSTER == 'devnet' ? 'devnet' : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
forceWithdrawTokens();
|
|
@ -29,6 +29,7 @@ const MAINNET_MINTS = new Map([
|
|||
['ETH', MINTS[1]],
|
||||
['SOL', MINTS[2]],
|
||||
['MNGO', MINTS[3]],
|
||||
['MSOL', MINTS[4]],
|
||||
]);
|
||||
|
||||
const STUB_PRICES = new Map([
|
||||
|
@ -36,13 +37,7 @@ const STUB_PRICES = new Map([
|
|||
['ETH', 1200.0], // eth and usdc both have 6 decimals
|
||||
['SOL', 0.015], // sol has 9 decimals, equivalent to $15 per SOL
|
||||
['MNGO', 0.02],
|
||||
]);
|
||||
|
||||
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
|
||||
// and verified to have best liquidity for pair on https://openserum.io/
|
||||
const MAINNET_SERUM3_MARKETS = new Map([
|
||||
['ETH/USDC', SERUM_MARKETS[0]],
|
||||
['SOL/USDC', SERUM_MARKETS[1]],
|
||||
['MSOL', 0.017],
|
||||
]);
|
||||
|
||||
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
|
||||
|
@ -90,11 +85,13 @@ async function main(): Promise<void> {
|
|||
for (const [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);
|
||||
if ((await client.getStubOracle(group, mintPk)).length == 0) {
|
||||
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}`);
|
||||
|
@ -114,22 +111,32 @@ async function main(): Promise<void> {
|
|||
maxRate: 1.5,
|
||||
};
|
||||
|
||||
const noFallbackOracle = PublicKey.default;
|
||||
|
||||
// register token 0
|
||||
console.log(`Registering USDC...`);
|
||||
const usdcMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
||||
const usdcOracle = oracles.get('USDC');
|
||||
try {
|
||||
await client.tokenRegister(group, usdcMint, usdcOracle, 0, 'USDC', {
|
||||
...DefaultTokenRegisterParams,
|
||||
loanOriginationFeeRate: 0,
|
||||
loanFeeRate: 0.0001,
|
||||
initAssetWeight: 1,
|
||||
maintAssetWeight: 1,
|
||||
initLiabWeight: 1,
|
||||
maintLiabWeight: 1,
|
||||
liquidationFee: 0,
|
||||
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
|
||||
});
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
usdcMint,
|
||||
usdcOracle,
|
||||
noFallbackOracle,
|
||||
0,
|
||||
'USDC',
|
||||
{
|
||||
...DefaultTokenRegisterParams,
|
||||
loanOriginationFeeRate: 0,
|
||||
loanFeeRate: 0.0001,
|
||||
initAssetWeight: 1,
|
||||
maintAssetWeight: 1,
|
||||
initLiabWeight: 1,
|
||||
maintLiabWeight: 1,
|
||||
liquidationFee: 0,
|
||||
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
|
||||
},
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
@ -140,17 +147,25 @@ async function main(): Promise<void> {
|
|||
const ethMint = new PublicKey(MAINNET_MINTS.get('ETH')!);
|
||||
const ethOracle = oracles.get('ETH');
|
||||
try {
|
||||
await client.tokenRegister(group, ethMint, ethOracle, 1, 'ETH', {
|
||||
...DefaultTokenRegisterParams,
|
||||
loanOriginationFeeRate: 0,
|
||||
loanFeeRate: 0.0001,
|
||||
maintAssetWeight: 0.9,
|
||||
initAssetWeight: 0.8,
|
||||
maintLiabWeight: 1.1,
|
||||
initLiabWeight: 1.2,
|
||||
liquidationFee: 0.05,
|
||||
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
|
||||
});
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
ethMint,
|
||||
ethOracle,
|
||||
noFallbackOracle,
|
||||
1,
|
||||
'ETH',
|
||||
{
|
||||
...DefaultTokenRegisterParams,
|
||||
loanOriginationFeeRate: 0,
|
||||
loanFeeRate: 0.0001,
|
||||
maintAssetWeight: 0.9,
|
||||
initAssetWeight: 0.8,
|
||||
maintLiabWeight: 1.1,
|
||||
initLiabWeight: 1.2,
|
||||
liquidationFee: 0.05,
|
||||
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
|
||||
},
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
@ -165,6 +180,7 @@ async function main(): Promise<void> {
|
|||
group,
|
||||
solMint,
|
||||
solOracle,
|
||||
noFallbackOracle,
|
||||
2, // tokenIndex
|
||||
'SOL',
|
||||
{
|
||||
|
@ -184,27 +200,72 @@ async function main(): Promise<void> {
|
|||
console.log(error);
|
||||
}
|
||||
|
||||
const genericBanks = ['MNGO', 'MSOL'];
|
||||
let nextTokenIndex = 3;
|
||||
for (const name of genericBanks) {
|
||||
console.log(`Registering ${name}...`);
|
||||
const mint = new PublicKey(MAINNET_MINTS.get(name)!);
|
||||
const oracle = oracles.get(name);
|
||||
try {
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
mint,
|
||||
oracle,
|
||||
noFallbackOracle,
|
||||
nextTokenIndex,
|
||||
name,
|
||||
{
|
||||
...DefaultTokenRegisterParams,
|
||||
loanOriginationFeeRate: 0,
|
||||
loanFeeRate: 0.0001,
|
||||
maintAssetWeight: 0.9,
|
||||
initAssetWeight: 0.8,
|
||||
maintLiabWeight: 1.1,
|
||||
initLiabWeight: 1.2,
|
||||
liquidationFee: 0.05,
|
||||
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
|
||||
},
|
||||
);
|
||||
nextTokenIndex += 1;
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
// log tokens/banks
|
||||
for (const bank of await group.banksMapByMint.values()) {
|
||||
console.log(`${bank.toString()}`);
|
||||
}
|
||||
|
||||
console.log('Registering SOL/USDC serum market...');
|
||||
try {
|
||||
await client.serum3RegisterMarket(
|
||||
group,
|
||||
new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!),
|
||||
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)),
|
||||
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)),
|
||||
1,
|
||||
'SOL/USDC',
|
||||
0,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
let nextSerumMarketIndex = 0;
|
||||
for (const [name, mint] of MAINNET_MINTS) {
|
||||
if (name == 'USDC') {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Registering ${name}/USDC serum market...`);
|
||||
try {
|
||||
await client.serum3RegisterMarket(
|
||||
group,
|
||||
new PublicKey(SERUM_MARKETS[nextSerumMarketIndex]),
|
||||
group.getFirstBankByMint(new PublicKey(mint)),
|
||||
group.getFirstBankByMint(usdcMint),
|
||||
nextSerumMarketIndex,
|
||||
`${name}/USDC`,
|
||||
0,
|
||||
);
|
||||
nextSerumMarketIndex += 1;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Registering MNGO-PERP market...');
|
||||
if (!group.banksMapByMint.get(usdcMint.toString())) {
|
||||
console.log('stopping, no USDC bank');
|
||||
return;
|
||||
}
|
||||
const mngoOracle = oracles.get('MNGO');
|
||||
try {
|
||||
await client.perpCreateMarket(
|
||||
|
@ -237,7 +298,7 @@ async function main(): Promise<void> {
|
|||
-1.0,
|
||||
2 * 60 * 60,
|
||||
0.025,
|
||||
0,
|
||||
0.0,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
|
|
@ -20,6 +20,9 @@ import { generateSerum3MarketExternalVaultSignerAddress } from '../../src/accoun
|
|||
// Script which creates three mints and two serum3 markets relating them
|
||||
//
|
||||
|
||||
const MINT_COUNT = 5;
|
||||
const SERUM_MARKET_COUNT = 4;
|
||||
|
||||
function getVaultOwnerAndNonce(
|
||||
market: PublicKey,
|
||||
programId: PublicKey,
|
||||
|
@ -56,7 +59,7 @@ async function main(): Promise<void> {
|
|||
|
||||
// Make mints
|
||||
const mints = await Promise.all(
|
||||
Array(4)
|
||||
Array(MINT_COUNT)
|
||||
.fill(null)
|
||||
.map(() =>
|
||||
splToken.createMint(connection, admin, admin.publicKey, null, 6),
|
||||
|
@ -78,11 +81,11 @@ async function main(): Promise<void> {
|
|||
// Make serum markets
|
||||
const serumMarkets: PublicKey[] = [];
|
||||
const quoteMint = mints[0];
|
||||
for (const baseMint of mints.slice(1, 3)) {
|
||||
for (const baseMint of mints.slice(1, 1 + SERUM_MARKET_COUNT)) {
|
||||
const feeRateBps = 0.25; // don't think this does anything
|
||||
const quoteDustThreshold = 100;
|
||||
const baseLotSize = 1000;
|
||||
const quoteLotSize = 1000;
|
||||
const quoteLotSize = 1; // makes prices be in 1000ths
|
||||
|
||||
const openbookProgramId = OPENBOOK_PROGRAM_ID.devnet;
|
||||
const market = Keypair.generate();
|
||||
|
|
|
@ -31,7 +31,7 @@ const CLUSTER = process.env.CLUSTER || 'mainnet-beta';
|
|||
// native prices
|
||||
const PRICES = {
|
||||
ETH: 1200.0,
|
||||
SOL: 0.015,
|
||||
SOL: 0.015, // not updated for the fact that the new mints we use have 6 decimals!
|
||||
USDC: 1,
|
||||
MNGO: 0.02,
|
||||
};
|
||||
|
@ -100,7 +100,7 @@ async function main() {
|
|||
async function createMangoAccount(name: string): Promise<MangoAccount> {
|
||||
const accountNum = maxAccountNum + 1;
|
||||
maxAccountNum = maxAccountNum + 1;
|
||||
await client.createMangoAccount(group, accountNum, name, 4, 4, 4, 4);
|
||||
await client.createMangoAccount(group, accountNum, name, 5, 4, 4, 4);
|
||||
return (await client.getMangoAccountForOwner(
|
||||
group,
|
||||
admin.publicKey,
|
||||
|
@ -202,7 +202,7 @@ async function main() {
|
|||
group,
|
||||
mangoAccount,
|
||||
sellMint,
|
||||
new BN(100000),
|
||||
new BN(150000),
|
||||
);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
|
@ -217,20 +217,40 @@ async function main() {
|
|||
.build(),
|
||||
);
|
||||
try {
|
||||
// At a price of $1/ui-SOL we can buy 0.1 ui-SOL for the 100k native-USDC we have.
|
||||
// With maint weight of 0.9 we have 10x main-leverage. Buying 12x as much causes liquidation.
|
||||
// At a price of $0.015/ui-SOL we can buy 10 ui-SOL for the 0.15 USDC (150k native-USDC) we have.
|
||||
// With maint weight of 0.9 we have 10x main-leverage. Buying 11x as much causes liquidation.
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
market.serumMarketExternal,
|
||||
Serum3Side.bid,
|
||||
1,
|
||||
12 * 0.1,
|
||||
0.015,
|
||||
11 * 10,
|
||||
Serum3SelfTradeBehavior.abortTransaction,
|
||||
Serum3OrderType.limit,
|
||||
0,
|
||||
5,
|
||||
);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
for (let market of group.serum3MarketsMapByMarketIndex.values()) {
|
||||
if (market.name == 'SOL/USDC') {
|
||||
continue;
|
||||
}
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
market.serumMarketExternal,
|
||||
Serum3Side.bid,
|
||||
0.001,
|
||||
1,
|
||||
Serum3SelfTradeBehavior.abortTransaction,
|
||||
Serum3OrderType.limit,
|
||||
0,
|
||||
5,
|
||||
);
|
||||
await mangoAccount.reload(client);
|
||||
}
|
||||
} finally {
|
||||
// restore the weights
|
||||
await client.tokenEdit(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
|
||||
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import * as splToken from '@solana/spl-token';
|
||||
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { Bank } from '../../src/accounts/bank';
|
||||
import {
|
||||
|
@ -280,6 +280,7 @@ async function main() {
|
|||
group,
|
||||
newMint,
|
||||
newOracle.publicKey,
|
||||
PublicKey.default,
|
||||
newTokenIndex,
|
||||
'TMP',
|
||||
{
|
||||
|
|
|
@ -57,6 +57,9 @@ async function main() {
|
|||
`closing serum orders on: ${account} for market ${serumMarket.name}`,
|
||||
);
|
||||
await client.serum3CancelAllOrders(group, account, serumExternal, 10);
|
||||
try {
|
||||
await client.serum3ConsumeEvents(group, serumExternal);
|
||||
} catch (e) {}
|
||||
await client.serum3SettleFunds(group, account, serumExternal);
|
||||
await client.serum3CloseOpenOrders(group, account, serumExternal);
|
||||
}
|
||||
|
|
|
@ -143,6 +143,7 @@ async function registerTokens(): Promise<void> {
|
|||
group,
|
||||
usdcMainnetMint,
|
||||
usdcMainnetOracle,
|
||||
PublicKey.default,
|
||||
0,
|
||||
'USDC',
|
||||
defaultTokenParams,
|
||||
|
|
|
@ -224,7 +224,7 @@ async function populateExistingAlts(): Promise<void> {
|
|||
.map((perpMarket) => [perpMarket.publicKey, perpMarket.oracle])
|
||||
.flat(),
|
||||
);
|
||||
// Well known addressess
|
||||
// Well known addresses
|
||||
await extendTable(
|
||||
client,
|
||||
group,
|
||||
|
|
|
@ -398,6 +398,9 @@ async function updateTokenParams(): Promise<void> {
|
|||
params.depositLimit,
|
||||
params.zeroUtilRate,
|
||||
params.platformLiquidationFee,
|
||||
params.disableAssetLiquidation,
|
||||
params.collateralFeePerDay,
|
||||
params.forceWithdraw,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
|
|
@ -82,6 +82,7 @@ export class Bank implements BankForHealth {
|
|||
public zeroUtilRate: I80F48;
|
||||
public platformLiquidationFee: I80F48;
|
||||
public collectedLiquidationFees: I80F48;
|
||||
public collectedCollateralFees: I80F48;
|
||||
|
||||
static from(
|
||||
publicKey: PublicKey,
|
||||
|
@ -129,6 +130,8 @@ export class Bank implements BankForHealth {
|
|||
depositWeightScaleStartQuote: number;
|
||||
reduceOnly: number;
|
||||
forceClose: number;
|
||||
disableAssetLiquidation: number;
|
||||
forceWithdraw: number;
|
||||
feesWithdrawn: BN;
|
||||
tokenConditionalSwapTakerFeeRate: number;
|
||||
tokenConditionalSwapMakerFeeRate: number;
|
||||
|
@ -146,6 +149,8 @@ export class Bank implements BankForHealth {
|
|||
zeroUtilRate: I80F48Dto;
|
||||
platformLiquidationFee: I80F48Dto;
|
||||
collectedLiquidationFees: I80F48Dto;
|
||||
collectedCollateralFees: I80F48Dto;
|
||||
collateralFeePerDay: number;
|
||||
},
|
||||
): Bank {
|
||||
return new Bank(
|
||||
|
@ -210,6 +215,10 @@ export class Bank implements BankForHealth {
|
|||
obj.zeroUtilRate,
|
||||
obj.platformLiquidationFee,
|
||||
obj.collectedLiquidationFees,
|
||||
obj.disableAssetLiquidation == 0,
|
||||
obj.collectedCollateralFees,
|
||||
obj.collateralFeePerDay,
|
||||
obj.forceWithdraw == 1,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -275,6 +284,10 @@ export class Bank implements BankForHealth {
|
|||
zeroUtilRate: I80F48Dto,
|
||||
platformLiquidationFee: I80F48Dto,
|
||||
collectedLiquidationFees: I80F48Dto,
|
||||
public allowAssetLiquidation: boolean,
|
||||
collectedCollateralFees: I80F48Dto,
|
||||
public collateralFeePerDay: number,
|
||||
public forceWithdraw: boolean,
|
||||
) {
|
||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||
this.oracleConfig = {
|
||||
|
@ -307,6 +320,7 @@ export class Bank implements BankForHealth {
|
|||
this.zeroUtilRate = I80F48.from(zeroUtilRate);
|
||||
this.platformLiquidationFee = I80F48.from(platformLiquidationFee);
|
||||
this.collectedLiquidationFees = I80F48.from(collectedLiquidationFees);
|
||||
this.collectedCollateralFees = I80F48.from(collectedCollateralFees);
|
||||
this._price = undefined;
|
||||
this._uiPrice = undefined;
|
||||
this._oracleLastUpdatedSlot = undefined;
|
||||
|
|
|
@ -55,6 +55,7 @@ export class Group {
|
|||
fastListingIntervalStart: BN;
|
||||
fastListingsInInterval: number;
|
||||
allowedFastListingsPerInterval: number;
|
||||
collateralFeeInterval: BN;
|
||||
},
|
||||
): Group {
|
||||
return new Group(
|
||||
|
@ -79,6 +80,7 @@ export class Group {
|
|||
obj.fastListingIntervalStart,
|
||||
obj.fastListingsInInterval,
|
||||
obj.allowedFastListingsPerInterval,
|
||||
obj.collateralFeeInterval,
|
||||
[], // addressLookupTablesList
|
||||
new Map(), // banksMapByName
|
||||
new Map(), // banksMapByMint
|
||||
|
@ -118,6 +120,7 @@ export class Group {
|
|||
public fastListingIntervalStart: BN,
|
||||
public fastListingsInInterval: number,
|
||||
public allowedFastListingsPerInterval: number,
|
||||
public collateralFeeInterval: BN,
|
||||
public addressLookupTablesList: AddressLookupTableAccount[],
|
||||
public banksMapByName: Map<string, Bank[]>,
|
||||
public banksMapByMint: Map<string, Bank[]>,
|
||||
|
|
|
@ -443,7 +443,7 @@ export class PerpMarket {
|
|||
/**
|
||||
*
|
||||
* Returns instantaneous funding rate for the day. How is it actually applied - funding is
|
||||
* continously applied on every interaction to a perp position. The rate is further multiplied
|
||||
* continuously applied on every interaction to a perp position. The rate is further multiplied
|
||||
* by the time elapsed since it was last applied (capped to max. 1hr).
|
||||
*
|
||||
* @param bids
|
||||
|
|
|
@ -7,8 +7,10 @@ import {
|
|||
} from '@coral-xyz/anchor';
|
||||
import { OpenOrders, decodeEventQueue } from '@project-serum/serum';
|
||||
import {
|
||||
createAccount,
|
||||
createCloseAccountInstruction,
|
||||
createInitializeAccount3Instruction,
|
||||
unpackAccount,
|
||||
} from '@solana/spl-token';
|
||||
import {
|
||||
AccountInfo,
|
||||
|
@ -24,6 +26,7 @@ import {
|
|||
RecentPrioritizationFees,
|
||||
SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
Signer,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
|
@ -322,6 +325,7 @@ export class MangoClient {
|
|||
feesMngoTokenIndex?: TokenIndex,
|
||||
feesExpiryInterval?: BN,
|
||||
allowedFastListingsPerInterval?: number,
|
||||
collateralFeeInterval?: BN,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const ix = await this.program.methods
|
||||
.groupEdit(
|
||||
|
@ -337,6 +341,7 @@ export class MangoClient {
|
|||
feesMngoTokenIndex ?? null,
|
||||
feesExpiryInterval ?? null,
|
||||
allowedFastListingsPerInterval ?? null,
|
||||
collateralFeeInterval ?? null,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
@ -443,6 +448,7 @@ export class MangoClient {
|
|||
group: Group,
|
||||
mintPk: PublicKey,
|
||||
oraclePk: PublicKey,
|
||||
fallbackOraclePk: PublicKey,
|
||||
tokenIndex: number,
|
||||
name: string,
|
||||
params: TokenRegisterParams,
|
||||
|
@ -478,12 +484,15 @@ export class MangoClient {
|
|||
params.depositLimit,
|
||||
params.zeroUtilRate,
|
||||
params.platformLiquidationFee,
|
||||
params.disableAssetLiquidation,
|
||||
params.collateralFeePerDay,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
mint: mintPk,
|
||||
oracle: oraclePk,
|
||||
fallbackOracle: fallbackOraclePk,
|
||||
payer: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
rent: SYSVAR_RENT_PUBKEY,
|
||||
})
|
||||
|
@ -560,14 +569,18 @@ export class MangoClient {
|
|||
params.maintWeightShiftAssetTarget,
|
||||
params.maintWeightShiftLiabTarget,
|
||||
params.maintWeightShiftAbort ?? false,
|
||||
params.setFallbackOracle ?? false,
|
||||
params.fallbackOracle !== null, // setFallbackOracle
|
||||
params.depositLimit,
|
||||
params.zeroUtilRate,
|
||||
params.platformLiquidationFee,
|
||||
params.disableAssetLiquidation,
|
||||
params.collateralFeePerDay,
|
||||
params.forceWithdraw,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
oracle: params.oracle ?? bank.oracle,
|
||||
fallbackOracle: params.fallbackOracle ?? bank.fallbackOracle,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
mintInfo: mintInfo.publicKey,
|
||||
})
|
||||
|
@ -629,6 +642,94 @@ export class MangoClient {
|
|||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
}
|
||||
|
||||
public async tokenForceWithdraw(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
tokenIndex: TokenIndex,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const bank = group.getFirstBankByTokenIndex(tokenIndex);
|
||||
if (!bank.forceWithdraw) {
|
||||
throw new Error('Bank is not in force-withdraw mode');
|
||||
}
|
||||
|
||||
const ownerAtaTokenAccount = await getAssociatedTokenAddress(
|
||||
bank.mint,
|
||||
mangoAccount.owner,
|
||||
true,
|
||||
);
|
||||
let alternateOwnerTokenAccount = PublicKey.default;
|
||||
const preInstructions: TransactionInstruction[] = [];
|
||||
const postInstructions: TransactionInstruction[] = [];
|
||||
|
||||
const ai = await this.connection.getAccountInfo(ownerAtaTokenAccount);
|
||||
|
||||
// ensure withdraws don't fail with missing ATAs
|
||||
if (ai == null) {
|
||||
preInstructions.push(
|
||||
await createAssociatedTokenAccountIdempotentInstruction(
|
||||
(this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
mangoAccount.owner,
|
||||
bank.mint,
|
||||
),
|
||||
);
|
||||
|
||||
// wsol case
|
||||
if (bank.mint.equals(NATIVE_MINT)) {
|
||||
postInstructions.push(
|
||||
createCloseAccountInstruction(
|
||||
ownerAtaTokenAccount,
|
||||
mangoAccount.owner,
|
||||
mangoAccount.owner,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const account = await unpackAccount(ownerAtaTokenAccount, ai);
|
||||
// if owner is not same as mango account's owner on the ATA (for whatever reason)
|
||||
// then create another token account
|
||||
if (!account.owner.equals(mangoAccount.owner)) {
|
||||
const kp = Keypair.generate();
|
||||
alternateOwnerTokenAccount = kp.publicKey;
|
||||
await createAccount(
|
||||
this.connection,
|
||||
(this.program.provider as AnchorProvider).wallet as any as Signer,
|
||||
bank.mint,
|
||||
mangoAccount.owner,
|
||||
kp,
|
||||
);
|
||||
|
||||
// wsol case
|
||||
if (bank.mint.equals(NATIVE_MINT)) {
|
||||
postInstructions.push(
|
||||
createCloseAccountInstruction(
|
||||
alternateOwnerTokenAccount,
|
||||
mangoAccount.owner,
|
||||
mangoAccount.owner,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ix = await this.program.methods
|
||||
.tokenForceWithdraw()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
bank: bank.publicKey,
|
||||
vault: bank.vault,
|
||||
oracle: bank.oracle,
|
||||
ownerAtaTokenAccount,
|
||||
alternateOwnerTokenAccount,
|
||||
})
|
||||
.instruction();
|
||||
return await this.sendAndConfirmTransactionForGroup(group, [
|
||||
...preInstructions,
|
||||
ix,
|
||||
...postInstructions,
|
||||
]);
|
||||
}
|
||||
|
||||
public async tokenDeregister(
|
||||
group: Group,
|
||||
mintPk: PublicKey,
|
||||
|
@ -737,16 +838,20 @@ export class MangoClient {
|
|||
mintPk: PublicKey,
|
||||
price: number,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const stubOracle = Keypair.generate();
|
||||
const ix = await this.program.methods
|
||||
.stubOracleCreate({ val: I80F48.fromNumber(price).getData() })
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
oracle: stubOracle.publicKey,
|
||||
mint: mintPk,
|
||||
payer: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
})
|
||||
.instruction();
|
||||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
return await this.sendAndConfirmTransactionForGroup(group, [ix], {
|
||||
additionalSigners: [stubOracle],
|
||||
});
|
||||
}
|
||||
|
||||
public async stubOracleClose(
|
||||
|
|
|
@ -30,6 +30,8 @@ export interface TokenRegisterParams {
|
|||
depositLimit: BN;
|
||||
zeroUtilRate: number;
|
||||
platformLiquidationFee: number;
|
||||
disableAssetLiquidation: boolean;
|
||||
collateralFeePerDay: number;
|
||||
}
|
||||
|
||||
export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||
|
@ -70,6 +72,8 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
|||
depositLimit: new BN(0),
|
||||
zeroUtilRate: 0.0,
|
||||
platformLiquidationFee: 0.0,
|
||||
disableAssetLiquidation: false,
|
||||
collateralFeePerDay: 0.0,
|
||||
};
|
||||
|
||||
export interface TokenEditParams {
|
||||
|
@ -107,10 +111,13 @@ export interface TokenEditParams {
|
|||
maintWeightShiftAssetTarget: number | null;
|
||||
maintWeightShiftLiabTarget: number | null;
|
||||
maintWeightShiftAbort: boolean | null;
|
||||
setFallbackOracle: boolean | null;
|
||||
fallbackOracle: PublicKey | null;
|
||||
depositLimit: BN | null;
|
||||
zeroUtilRate: number | null;
|
||||
platformLiquidationFee: number | null;
|
||||
disableAssetLiquidation: boolean | null;
|
||||
collateralFeePerDay: number | null;
|
||||
forceWithdraw: boolean | null;
|
||||
}
|
||||
|
||||
export const NullTokenEditParams: TokenEditParams = {
|
||||
|
@ -148,10 +155,13 @@ export const NullTokenEditParams: TokenEditParams = {
|
|||
maintWeightShiftAssetTarget: null,
|
||||
maintWeightShiftLiabTarget: null,
|
||||
maintWeightShiftAbort: null,
|
||||
setFallbackOracle: null,
|
||||
fallbackOracle: null,
|
||||
depositLimit: null,
|
||||
zeroUtilRate: null,
|
||||
platformLiquidationFee: null,
|
||||
disableAssetLiquidation: null,
|
||||
collateralFeePerDay: null,
|
||||
forceWithdraw: null,
|
||||
};
|
||||
|
||||
export interface PerpEditParams {
|
||||
|
@ -299,6 +309,7 @@ export interface IxGateParams {
|
|||
TokenConditionalSwapCreatePremiumAuction: boolean;
|
||||
TokenConditionalSwapCreateLinearAuction: boolean;
|
||||
Serum3PlaceOrderV2: boolean;
|
||||
TokenForceWithdraw: boolean;
|
||||
}
|
||||
|
||||
// Default with all ixs enabled, use with buildIxGate
|
||||
|
@ -378,6 +389,7 @@ export const TrueIxGateParams: IxGateParams = {
|
|||
TokenConditionalSwapCreatePremiumAuction: true,
|
||||
TokenConditionalSwapCreateLinearAuction: true,
|
||||
Serum3PlaceOrderV2: true,
|
||||
TokenForceWithdraw: true,
|
||||
};
|
||||
|
||||
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
|
||||
|
@ -467,6 +479,7 @@ export function buildIxGate(p: IxGateParams): BN {
|
|||
toggleIx(ixGate, p, 'TokenConditionalSwapCreatePremiumAuction', 69);
|
||||
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
|
||||
toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71);
|
||||
toggleIx(ixGate, p, 'TokenForceWithdraw', 72);
|
||||
|
||||
return ixGate;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export type MangoV4 = {
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -277,6 +277,12 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"option": "u16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeeIntervalOpt",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -631,6 +637,14 @@ export type MangoV4 = {
|
|||
{
|
||||
"name": "platformLiquidationFee",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "disableAssetLiquidation",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "collateralFeePerDay",
|
||||
"type": "f32"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1041,6 +1055,24 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disableAssetLiquidationOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeePerDayOpt",
|
||||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "forceWithdrawOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3763,6 +3795,63 @@ export type MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenForceWithdraw",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bank",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group",
|
||||
"vault",
|
||||
"oracle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "ownerAtaTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "alternateOwnerTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"Only for the unusual case where the owner_ata account is not owned by account.owner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "perpCreateMarket",
|
||||
"docs": [
|
||||
|
@ -5953,6 +6042,25 @@ export type MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenChargeCollateralFees",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "altSet",
|
||||
"accounts": [
|
||||
|
@ -7373,12 +7481,24 @@ export type MangoV4 = {
|
|||
"name": "forceClose",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "disableAssetLiquidation",
|
||||
"docs": [
|
||||
"If set to 1, deposits cannot be liquidated when an account is liquidatable.",
|
||||
"That means bankrupt accounts may still have assets of this type deposited."
|
||||
],
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "forceWithdraw",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
6
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -7513,12 +7633,30 @@ export type MangoV4 = {
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collectedCollateralFees",
|
||||
"docs": [
|
||||
"Collateral fees that have been collected (in native tokens)",
|
||||
"",
|
||||
"See also collected_fees_native and fees_withdrawn."
|
||||
],
|
||||
"type": {
|
||||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeePerDay",
|
||||
"docs": [
|
||||
"The daily collateral fees rate for fully utilized collateral."
|
||||
],
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1920
|
||||
1900
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -7646,12 +7784,28 @@ export type MangoV4 = {
|
|||
],
|
||||
"type": "u16"
|
||||
},
|
||||
{
|
||||
"name": "padding2",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeeInterval",
|
||||
"docs": [
|
||||
"Intervals in which collateral fee is applied"
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1812
|
||||
1800
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -7773,12 +7927,27 @@ export type MangoV4 = {
|
|||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "temporaryDelegate",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "temporaryDelegateExpiry",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "lastCollateralFeeCharge",
|
||||
"docs": [
|
||||
"Time at which the last collateral fee was charged"
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
200
|
||||
152
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -9548,12 +9717,16 @@ export type MangoV4 = {
|
|||
"name": "temporaryDelegateExpiry",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "lastCollateralFeeCharge",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
160
|
||||
152
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -10474,6 +10647,9 @@ export type MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "Swap"
|
||||
},
|
||||
{
|
||||
"name": "SwapWithoutFee"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -10829,6 +11005,9 @@ export type MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "Serum3PlaceOrderV2"
|
||||
},
|
||||
{
|
||||
"name": "TokenForceWithdraw"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -13746,6 +13925,76 @@ export type MangoV4 = {
|
|||
"index": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TokenCollateralFeeLog",
|
||||
"fields": [
|
||||
{
|
||||
"name": "mangoGroup",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "mangoAccount",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "tokenIndex",
|
||||
"type": "u16",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "assetUsageFraction",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "fee",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ForceWithdrawLog",
|
||||
"fields": [
|
||||
{
|
||||
"name": "mangoGroup",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "mangoAccount",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "tokenIndex",
|
||||
"type": "u16",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "quantity",
|
||||
"type": "u64",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "toTokenAccount",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
|
@ -14093,12 +14342,17 @@ export type MangoV4 = {
|
|||
"code": 6068,
|
||||
"name": "MissingFeedForCLMMOracle",
|
||||
"msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)"
|
||||
},
|
||||
{
|
||||
"code": 6069,
|
||||
"name": "TokenAssetLiquidationDisabled",
|
||||
"msg": "the asset does not allow liquidation"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const IDL: MangoV4 = {
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -14376,6 +14630,12 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"option": "u16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeeIntervalOpt",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -14730,6 +14990,14 @@ export const IDL: MangoV4 = {
|
|||
{
|
||||
"name": "platformLiquidationFee",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "disableAssetLiquidation",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "collateralFeePerDay",
|
||||
"type": "f32"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -15140,6 +15408,24 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disableAssetLiquidationOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeePerDayOpt",
|
||||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "forceWithdrawOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -17862,6 +18148,63 @@ export const IDL: MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenForceWithdraw",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bank",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group",
|
||||
"vault",
|
||||
"oracle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "ownerAtaTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "alternateOwnerTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"Only for the unusual case where the owner_ata account is not owned by account.owner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "perpCreateMarket",
|
||||
"docs": [
|
||||
|
@ -20052,6 +20395,25 @@ export const IDL: MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenChargeCollateralFees",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "altSet",
|
||||
"accounts": [
|
||||
|
@ -21472,12 +21834,24 @@ export const IDL: MangoV4 = {
|
|||
"name": "forceClose",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "disableAssetLiquidation",
|
||||
"docs": [
|
||||
"If set to 1, deposits cannot be liquidated when an account is liquidatable.",
|
||||
"That means bankrupt accounts may still have assets of this type deposited."
|
||||
],
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "forceWithdraw",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
6
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -21612,12 +21986,30 @@ export const IDL: MangoV4 = {
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collectedCollateralFees",
|
||||
"docs": [
|
||||
"Collateral fees that have been collected (in native tokens)",
|
||||
"",
|
||||
"See also collected_fees_native and fees_withdrawn."
|
||||
],
|
||||
"type": {
|
||||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeePerDay",
|
||||
"docs": [
|
||||
"The daily collateral fees rate for fully utilized collateral."
|
||||
],
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1920
|
||||
1900
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -21745,12 +22137,28 @@ export const IDL: MangoV4 = {
|
|||
],
|
||||
"type": "u16"
|
||||
},
|
||||
{
|
||||
"name": "padding2",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "collateralFeeInterval",
|
||||
"docs": [
|
||||
"Intervals in which collateral fee is applied"
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1812
|
||||
1800
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -21872,12 +22280,27 @@ export const IDL: MangoV4 = {
|
|||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "temporaryDelegate",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "temporaryDelegateExpiry",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "lastCollateralFeeCharge",
|
||||
"docs": [
|
||||
"Time at which the last collateral fee was charged"
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
200
|
||||
152
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -23647,12 +24070,16 @@ export const IDL: MangoV4 = {
|
|||
"name": "temporaryDelegateExpiry",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "lastCollateralFeeCharge",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
160
|
||||
152
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -24573,6 +25000,9 @@ export const IDL: MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "Swap"
|
||||
},
|
||||
{
|
||||
"name": "SwapWithoutFee"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -24928,6 +25358,9 @@ export const IDL: MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "Serum3PlaceOrderV2"
|
||||
},
|
||||
{
|
||||
"name": "TokenForceWithdraw"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -27845,6 +28278,76 @@ export const IDL: MangoV4 = {
|
|||
"index": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TokenCollateralFeeLog",
|
||||
"fields": [
|
||||
{
|
||||
"name": "mangoGroup",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "mangoAccount",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "tokenIndex",
|
||||
"type": "u16",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "assetUsageFraction",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "fee",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ForceWithdrawLog",
|
||||
"fields": [
|
||||
{
|
||||
"name": "mangoGroup",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "mangoAccount",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "tokenIndex",
|
||||
"type": "u16",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "quantity",
|
||||
"type": "u64",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "toTokenAccount",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
|
@ -28192,6 +28695,11 @@ export const IDL: MangoV4 = {
|
|||
"code": 6068,
|
||||
"name": "MissingFeedForCLMMOracle",
|
||||
"msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)"
|
||||
},
|
||||
{
|
||||
"code": 6069,
|
||||
"name": "TokenAssetLiquidationDisabled",
|
||||
"msg": "the asset does not allow liquidation"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
@ -9,11 +9,13 @@ export class FlashLoanWithdraw {
|
|||
|
||||
export type FlashLoanType =
|
||||
| { unknown: Record<string, never> }
|
||||
| { swap: Record<string, never> };
|
||||
| { swap: Record<string, never> }
|
||||
| { swapWithoutFee: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace FlashLoanType {
|
||||
export const unknown = { unknown: {} };
|
||||
export const swap = { swap: {} };
|
||||
export const swapWithoutFee = { swapWithoutFee: {} };
|
||||
}
|
||||
|
||||
export class InterestRateParams {
|
||||
|
|
Loading…
Reference in New Issue