deploy -> dev (#759)
* ts: get yarn lock from dev Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.19.20 * ts: add missing dependency Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * ts: add error when no free token position is found (#707) Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Mc/tcs improvements (#706) * ts: additional tcs helpers Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Revert "Fixes from review" This reverts commit 1def10353511802c030a100fd23b2c2f4f198eaa. --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.19.21 * v0.19.22 * ts: tcs fix price display input to tx Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.19.23 * v0.19.25 * script: log all Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * ts: fix tcs order price limits Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.19.27 * ts: fix getTimeToNextBorrowLimitWindowStartsTs (#710) * ts: fix getTimeToNextBorrowLimitWindowStartsTs Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Mc/keeper (#714) * v0.19.28 * ts: tokenWithdrawAllDepositForMint Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fix Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fix Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * rust: dont include tokens with errors in crank Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * review fixes * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> Co-authored-by: Christian Kamm <mail@ckamm.de> * v0.19.29 * ts: update debug script Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * release 19.1 -> deploy + serum3 open orders estimation ts patch (#719) * Serum3 open orders: Fix health overestimation (#716) When bids or asks crossed the oracle price, the serum3 health would be overestimated before. The health code has no access to the open order quantites or prices and used to assume all orders are at oracle price. Now we track an account's max bid and min ask in each market and use that as a worst-case price. The tracking isn't perfect for technical reasons (compute cost, no notifications on fill) but produces an upper bound on bids (lower bound on asks) that is sufficient to make health not overestimate. The tracked price is reset every time the serum3 open orders on a book side are completely cleared. (cherry picked from commit2adc0339dc
) * Changelog, version bump for program v0.19.1 * ts: ts patch for the PR Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> Co-authored-by: Christian Kamm <mail@ckamm.de> * Rust client: Use alts for every transaction (#720) (cherry picked from commit40ad0b7b66
) * Jupiter: ensure source account is initialized Backport of9b224eae1b
/ #721 * client/liquidator: jupiter v6 (#684) Add rust client functions for v6 API that are usuable in parallel to the v4 ones. (cherry picked from commit0f10cb4d92
) * Jupiter: Ensure source account is initialized (#721) (cherry picked from commit9b224eae1b
) * Mc/update cu budget for perp settle pnl (#724) * ts: bump perp settle pnl cu budget Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * ts: helpers for withdrawing tokens from bad oracles (#726) * ts: helpers for withdrawing tokens from bad oracles Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * rename Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * rename Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fix usage of field Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.19.31 * ts: higher min. cu limit for each tx (#727) Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.19.32 * ts: if more ixs then more cu (#728) Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Mc/tcs p95 (#708) * use more fine grain price impact Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * ts: for computing tcs premium use more fine grain price impact Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * update Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Mc/settler cu limit (#725) * v0.19.30 * settler: extend cu limit to 250k for perp pnl settling Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * TransactionBuilder: add cu limit/price based on config --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> Co-authored-by: Christian Kamm <mail@ckamm.de> * ts: rename params to indicate that they are in native Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * ts: cleanup tcs create parameter naming (#730) Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * wip: Mc/update risk params (#729) * v0.19.33 * ts: script to update risk params Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * create proposals helpers * fix * Update env params Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Update Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Update Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * simulate before run * fix presets * fix --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> Co-authored-by: Adrian Brzeziński <a.brzezinski94@gmail.com> * ts: upgrade anchor (#735) * ts: upgrade anchor Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * script for tx error grouping, and ts helper code for finding tx error reason (#747) Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.19.34 * ts: fix script for updating token params Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fix typo Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * script: update script to remove files which are of 0 size Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * script: error tx grouping, blacklist some more Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * fix (#753) * jupiter: clearer slippage_bps argument name --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> Co-authored-by: Christian Kamm <mail@ckamm.de> Co-authored-by: Adrian Brzeziński <a.brzezinski94@gmail.com>
This commit is contained in:
parent
b123a3c5ae
commit
edaf874174
|
@ -14,3 +14,5 @@ yarn-error.log
|
|||
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
err-txs
|
|
@ -129,6 +129,7 @@ impl Rpc {
|
|||
None,
|
||||
TransactionBuilderConfig {
|
||||
prioritization_micro_lamports: Some(5),
|
||||
compute_budget_per_instruction: Some(250_000),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
|
|
@ -217,7 +217,22 @@ pub async fn loop_update_index_and_rate(
|
|||
is_writable: true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
ix.accounts.append(&mut banks);
|
||||
|
||||
let sim_result = match client.simulate(vec![ix.clone()]).await {
|
||||
Ok(response) => response.value,
|
||||
Err(e) => {
|
||||
error!(token.name, "simulation request error: {e:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(e) = sim_result.err {
|
||||
error!(token.name, "simulation error: {e:?} {:?}", sim_result.logs);
|
||||
continue;
|
||||
}
|
||||
|
||||
instructions.push(ix);
|
||||
}
|
||||
let pre = Instant::now();
|
||||
|
|
|
@ -106,6 +106,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
TransactionBuilderConfig {
|
||||
prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0)
|
||||
.then_some(cli.prioritization_micro_lamports),
|
||||
compute_budget_per_instruction: None,
|
||||
},
|
||||
),
|
||||
cli.mango_account,
|
||||
|
|
|
@ -171,6 +171,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
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),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -74,9 +74,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
CommitmentConfig::processed(),
|
||||
Arc::new(Keypair::new()),
|
||||
Some(rpc_timeout),
|
||||
TransactionBuilderConfig {
|
||||
prioritization_micro_lamports: None,
|
||||
},
|
||||
TransactionBuilderConfig::default(),
|
||||
);
|
||||
let group_pk = Pubkey::from_str(&config.mango_group).unwrap();
|
||||
let group_context =
|
||||
|
|
|
@ -369,9 +369,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
CommitmentConfig::processed(),
|
||||
Arc::new(Keypair::new()),
|
||||
Some(rpc_timeout),
|
||||
TransactionBuilderConfig {
|
||||
prioritization_micro_lamports: None,
|
||||
},
|
||||
TransactionBuilderConfig::default(),
|
||||
);
|
||||
let group_context = Arc::new(
|
||||
MangoGroupContext::new_from_rpc(
|
||||
|
|
|
@ -353,9 +353,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
CommitmentConfig::processed(),
|
||||
Arc::new(Keypair::new()),
|
||||
Some(rpc_timeout),
|
||||
TransactionBuilderConfig {
|
||||
prioritization_micro_lamports: None,
|
||||
},
|
||||
TransactionBuilderConfig::default(),
|
||||
);
|
||||
let group_context = Arc::new(
|
||||
MangoGroupContext::new_from_rpc(
|
||||
|
|
|
@ -262,9 +262,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
commitment,
|
||||
Arc::new(Keypair::new()),
|
||||
Some(rpc_timeout),
|
||||
TransactionBuilderConfig {
|
||||
prioritization_micro_lamports: None,
|
||||
},
|
||||
TransactionBuilderConfig::default(),
|
||||
);
|
||||
let group_context = Arc::new(
|
||||
MangoGroupContext::new_from_rpc(
|
||||
|
|
|
@ -64,6 +64,10 @@ struct Cli {
|
|||
/// prioritize each transaction with this many microlamports/cu
|
||||
#[clap(long, env, default_value = "0")]
|
||||
prioritization_micro_lamports: u64,
|
||||
|
||||
/// compute budget for each instruction
|
||||
#[clap(long, env, default_value = "250000")]
|
||||
compute_budget_per_instruction: u32,
|
||||
}
|
||||
|
||||
pub fn encode_address(addr: &Pubkey) -> String {
|
||||
|
@ -99,6 +103,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
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),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ use anchor_client::Cluster;
|
|||
|
||||
use anchor_lang::__private::bytemuck;
|
||||
use anchor_lang::prelude::System;
|
||||
use anchor_lang::{AccountDeserialize, Id};
|
||||
use anchor_lang::{AccountDeserialize, AnchorDeserialize, Id};
|
||||
use anchor_spl::associated_token::get_associated_token_address;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
|
@ -25,6 +25,7 @@ use mango_v4::state::{
|
|||
use solana_address_lookup_table_program::state::AddressLookupTable;
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_client::rpc_config::RpcSendTransactionConfig;
|
||||
use solana_client::rpc_response::RpcSimulateTransactionResult;
|
||||
use solana_sdk::address_lookup_table_account::AddressLookupTableAccount;
|
||||
use solana_sdk::commitment_config::CommitmentLevel;
|
||||
use solana_sdk::hash::Hash;
|
||||
|
@ -1566,6 +1567,21 @@ impl MangoClient {
|
|||
.send_and_confirm(&self.client)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn simulate(
|
||||
&self,
|
||||
instructions: Vec<Instruction>,
|
||||
) -> anyhow::Result<SimulateTransactionResponse> {
|
||||
TransactionBuilder {
|
||||
instructions,
|
||||
address_lookup_tables: vec![],
|
||||
payer: self.client.fee_payer.pubkey(),
|
||||
signers: vec![self.client.fee_payer.clone()],
|
||||
config: self.client.transaction_builder_config,
|
||||
}
|
||||
.simulate(&self.client)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
@ -1579,7 +1595,7 @@ pub enum MangoClientError {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct TransactionSize {
|
||||
pub accounts: usize,
|
||||
pub length: usize,
|
||||
|
@ -1599,10 +1615,12 @@ impl TransactionSize {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct TransactionBuilderConfig {
|
||||
// adds a SetComputeUnitPrice instruction in front
|
||||
// adds a SetComputeUnitPrice instruction in front if none exists
|
||||
pub prioritization_micro_lamports: Option<u64>,
|
||||
// adds a SetComputeUnitBudget instruction if none exists
|
||||
pub compute_budget_per_instruction: Option<u32>,
|
||||
}
|
||||
|
||||
pub struct TransactionBuilder {
|
||||
|
@ -1613,6 +1631,9 @@ pub struct TransactionBuilder {
|
|||
pub config: TransactionBuilderConfig,
|
||||
}
|
||||
|
||||
pub type SimulateTransactionResponse =
|
||||
solana_client::rpc_response::Response<RpcSimulateTransactionResult>;
|
||||
|
||||
impl TransactionBuilder {
|
||||
pub async fn transaction(
|
||||
&self,
|
||||
|
@ -1622,22 +1643,54 @@ impl TransactionBuilder {
|
|||
self.transaction_with_blockhash(latest_blockhash)
|
||||
}
|
||||
|
||||
fn instructions_with_cu_budget(&self) -> Vec<Instruction> {
|
||||
use solana_sdk::compute_budget::{self, ComputeBudgetInstruction};
|
||||
let mut ixs = self.instructions.clone();
|
||||
|
||||
let mut has_compute_unit_price = false;
|
||||
let mut has_compute_unit_limit = false;
|
||||
let mut cu_instructions = 0;
|
||||
for ix in ixs.iter() {
|
||||
if ix.program_id != compute_budget::id() {
|
||||
continue;
|
||||
}
|
||||
cu_instructions += 1;
|
||||
match ComputeBudgetInstruction::try_from_slice(&ix.data) {
|
||||
Ok(ComputeBudgetInstruction::SetComputeUnitLimit(_)) => {
|
||||
has_compute_unit_limit = true
|
||||
}
|
||||
Ok(ComputeBudgetInstruction::SetComputeUnitPrice(_)) => {
|
||||
has_compute_unit_price = true
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let cu_per_ix = self.config.compute_budget_per_instruction.unwrap_or(0);
|
||||
if !has_compute_unit_limit && cu_per_ix > 0 {
|
||||
let ix_count: u32 = (ixs.len() - cu_instructions).try_into().unwrap();
|
||||
ixs.insert(
|
||||
0,
|
||||
ComputeBudgetInstruction::set_compute_unit_limit(cu_per_ix * ix_count),
|
||||
);
|
||||
}
|
||||
|
||||
let cu_prio = self.config.prioritization_micro_lamports.unwrap_or(0);
|
||||
if !has_compute_unit_price && cu_prio > 0 {
|
||||
ixs.insert(0, ComputeBudgetInstruction::set_compute_unit_price(cu_prio));
|
||||
}
|
||||
|
||||
ixs
|
||||
}
|
||||
|
||||
pub fn transaction_with_blockhash(
|
||||
&self,
|
||||
blockhash: Hash,
|
||||
) -> anyhow::Result<solana_sdk::transaction::VersionedTransaction> {
|
||||
let mut ix = self.instructions.clone();
|
||||
if let Some(prio_price) = self.config.prioritization_micro_lamports {
|
||||
ix.insert(
|
||||
0,
|
||||
solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price(
|
||||
prio_price,
|
||||
),
|
||||
)
|
||||
}
|
||||
let ixs = self.instructions_with_cu_budget();
|
||||
let v0_message = solana_sdk::message::v0::Message::try_compile(
|
||||
&self.payer,
|
||||
&ix,
|
||||
&ixs,
|
||||
&self.address_lookup_tables,
|
||||
blockhash,
|
||||
)?;
|
||||
|
@ -1663,6 +1716,12 @@ impl TransactionBuilder {
|
|||
.map_err(prettify_solana_client_error)
|
||||
}
|
||||
|
||||
pub async fn simulate(&self, client: &Client) -> anyhow::Result<SimulateTransactionResponse> {
|
||||
let rpc = client.rpc_async();
|
||||
let tx = self.transaction(&rpc).await?;
|
||||
Ok(rpc.simulate_transaction(&tx).await?)
|
||||
}
|
||||
|
||||
pub async fn send_and_confirm(&self, client: &Client) -> anyhow::Result<Signature> {
|
||||
let rpc = client.rpc_async();
|
||||
let tx = self.transaction(&rpc).await?;
|
||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@blockworks-foundation/mango-v4",
|
||||
"version": "0.19.14",
|
||||
"version": "0.19.34",
|
||||
"description": "Typescript Client for mango-v4 program.",
|
||||
"repository": "https://github.com/blockworks-foundation/mango-v4",
|
||||
"author": {
|
||||
|
@ -48,10 +48,13 @@
|
|||
"eslint-config-prettier": "^7.2.0",
|
||||
"fast-csv": "^4.3.6",
|
||||
"lodash": "^4.17.21",
|
||||
"js-sha3": "0.9.2",
|
||||
"mocha": "^9.1.3",
|
||||
"prettier": "^2.0.5",
|
||||
"secp256k1": "5.0.0",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tweetnacl": "1.0.3",
|
||||
"typedoc": "^0.22.5",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
|
@ -60,7 +63,8 @@
|
|||
"trailingComma": "all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor": "^0.27.0",
|
||||
"@blockworks-foundation/mango-v4-settings": "^0.2.16",
|
||||
"@coral-xyz/anchor": "^0.28.1-beta.2",
|
||||
"@project-serum/serum": "0.13.65",
|
||||
"@pythnetwork/client": "~2.14.0",
|
||||
"@solana/spl-token": "0.3.7",
|
||||
|
@ -71,10 +75,11 @@
|
|||
"bs58": "^5.0.0",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"node-kraken-api": "^2.2.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"@coral-xyz/anchor": "^0.27.0",
|
||||
"@coral-xyz/anchor": "^0.28.1-beta.2",
|
||||
"**/@solana/web3.js/node-fetch": "npm:@blockworks-foundation/node-fetch@2.6.11",
|
||||
"**/cross-fetch/node-fetch": "npm:@blockworks-foundation/node-fetch@2.6.11"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/env zsh
|
||||
|
||||
RPC_URL=$MB_CLUSTER_URL
|
||||
|
||||
REQUEST_BODY=$(cat <<- \END
|
||||
{"jsonrpc": "2.0", "id": 1, "method": "getTransaction", "params": [
|
||||
"{}",
|
||||
{"commitment": "confirmed", "encoding": "json", "maxSupportedTransactionVersion": 0}
|
||||
]}
|
||||
END
|
||||
)
|
||||
|
||||
|
||||
mkdir -p err-txs/original
|
||||
mkdir -p err-txs/bak
|
||||
mkdir -p err-txs/processed
|
||||
|
||||
rm -rf err-txs/processed/*
|
||||
|
||||
# File containing last n number of error signatures, fetch txs
|
||||
# cat ~/Downloads/err-tx-sigs | parallel -k -j8 "curl -sS -X POST $RPC_URL -H 'Content-type: application/json' -d'$REQUEST_BODY' > err-txs/original/{}"
|
||||
|
||||
# Ignore market makers
|
||||
cp err-txs/original/* err-txs/bak/
|
||||
grep -rl '4hXPGTmR6dKNNqjLYdfDRSrTaa1Wt2GZoZnQ9hAJEeev' err-txs/bak | xargs rm
|
||||
grep -rl 'BLgb4NFwhpurMrGX5LQfb8D8dBpGSGtBqqew2Em8uyRT' err-txs/bak | xargs rm
|
||||
grep -rl '2f4nvyfS47tL8XaMt1Nm8kiE6dPW2W5udoRSWZch1bK9' err-txs/bak | xargs rm
|
||||
|
||||
|
||||
# Extract logs for easy viewing
|
||||
find err-txs/bak/* | xargs -I % basename % | parallel "jq '.result.meta.logMessages' err-txs/bak/{} > err-txs/processed/{}"
|
||||
|
||||
# Ignore known errors
|
||||
grep -rl 'out of order' err-txs/processed | xargs rm
|
||||
grep -rl 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX failed: custom program error: 0x2a' err-txs/processed | xargs rm
|
||||
grep -rl 'programs/dyson/src/instructions/swap_arb' err-txs/processed | xargs rm
|
||||
grep -rl 'programs/dyson/src/instructions/swap_protected' err-txs/processed | xargs rm
|
||||
grep -rl 'SlippageToleranceExceeded' err-txs/processed | xargs rm
|
||||
grep -rl 'InvalidCalculation' err-txs/processed | xargs rm
|
||||
grep -rl 'RaydiumSwapExactOutput' err-txs/processed | xargs rm
|
||||
grep -rl 'RaydiumClmmSwapExactOutput' err-txs/processed | xargs rm
|
||||
grep -rl 'SharedAccountsExactOutRoute' err-txs/processed | xargs rm
|
||||
grep -rl 'OracleStale' err-txs/processed | xargs rm
|
||||
grep -rl 'AccountCreate' err-txs/processed | xargs rm
|
||||
grep -rl 'ProfitabilityMismatch' err-txs/processed | xargs rm
|
||||
grep -rl 'token is in reduce only mode' err-txs/processed | xargs rm
|
||||
|
||||
|
||||
find err-txs/processed/ -size 0 -print -delete
|
|
@ -38,6 +38,10 @@ async function main(): Promise<void> {
|
|||
);
|
||||
|
||||
let account = await client.getMangoAccount(new PublicKey(MANGO_ACCOUNT_PK));
|
||||
account
|
||||
.tokenConditionalSwapsActive()
|
||||
.forEach((tcs) => console.log(tcs.toString(group)));
|
||||
|
||||
await Promise.all(
|
||||
account.tokenConditionalSwaps.map((tcs, i) => {
|
||||
if (!tcs.hasData) {
|
||||
|
|
|
@ -284,21 +284,7 @@ async function main(): Promise<void> {
|
|||
mangoAccounts.sort((a, b) => b.getEquity(group).cmp(a.getEquity(group)));
|
||||
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
if (
|
||||
true &&
|
||||
(MANGO_ACCOUNT_PK!.equals(PublicKey.default) ||
|
||||
// For specific account
|
||||
mangoAccount.publicKey.equals(new PublicKey(MANGO_ACCOUNT_PK!))) &&
|
||||
// Only interesting perp liq price candidates
|
||||
mangoAccount.perpActive().length > 0 &&
|
||||
mangoAccount
|
||||
.perpActive()
|
||||
.filter((pp) =>
|
||||
pp
|
||||
.getBasePosition(group.getPerpMarketByMarketIndex(pp.marketIndex))
|
||||
.gt(ZERO_I80F48()),
|
||||
).length > 0
|
||||
) {
|
||||
if (mangoAccount.publicKey.equals(new PublicKey(MANGO_ACCOUNT_PK!))) {
|
||||
console.log(
|
||||
`account https://app.mango.markets/?address=${mangoAccount.publicKey}`,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
|
||||
export const MANGO_REALM_PK = new PublicKey(
|
||||
'DPiH3H3c7t47BMxqTxLsuPQpEC6Kne8GA9VXbxpnZxFE',
|
||||
);
|
||||
export const MANGO_GOVERNANCE_PROGRAM = new PublicKey(
|
||||
'GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J',
|
||||
);
|
||||
|
||||
export const VOTER_INFO_EVENT_NAME = 'VoterInfo';
|
||||
export const DEPOSIT_EVENT_NAME = 'DepositEntryInfo';
|
||||
// The wallet can be any existing account for the simulation
|
||||
// Note: when running a local validator ensure the account is copied from devnet: --clone ENmcpFCpxN1CqyUjuog9yyUVfdXBKF3LVCwLr7grJZpk -ud
|
||||
export const SIMULATION_WALLET = 'ENmcpFCpxN1CqyUjuog9yyUVfdXBKF3LVCwLr7grJZpk';
|
||||
|
||||
export const MANGO_MINT = new PublicKey(
|
||||
'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac',
|
||||
);
|
||||
|
||||
export const MANGO_DAO_WALLET_GOVERNANCE = new PublicKey(
|
||||
'7zGXUAeUkY9pEGfApsY26amibvqsf2dmty1cbtxHdfaQ',
|
||||
);
|
||||
export const MANGO_DAO_WALLET = new PublicKey(
|
||||
'5tgfd6XgwiXB9otEnzFpXK11m7Q7yZUaAJzWK4oT5UGF',
|
||||
);
|
||||
|
||||
export const MANGO_MINT_DECIMALS = 6;
|
||||
|
||||
export const MAINNET_PYTH_PROGRAM = new PublicKey(
|
||||
'FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH',
|
||||
);
|
||||
export const DEVNET_PYTH_PROGRAM = new PublicKey(
|
||||
'gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s',
|
||||
);
|
|
@ -0,0 +1,161 @@
|
|||
import {
|
||||
getGovernanceProgramVersion,
|
||||
getInstructionDataFromBase64,
|
||||
getSignatoryRecordAddress,
|
||||
ProgramAccount,
|
||||
serializeInstructionToBase64,
|
||||
TokenOwnerRecord,
|
||||
VoteType,
|
||||
WalletSigner,
|
||||
withAddSignatory,
|
||||
withCreateProposal,
|
||||
withInsertTransaction,
|
||||
withSignOffProposal,
|
||||
} from '@solana/spl-governance';
|
||||
import {
|
||||
Connection,
|
||||
PublicKey,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import chunk from 'lodash/chunk';
|
||||
import { updateVoterWeightRecord } from './updateVoteWeightRecord';
|
||||
import { VsrClient } from './voteStakeRegistryClient';
|
||||
|
||||
export const MANGO_MINT = 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac';
|
||||
export const MANGO_REALM_PK = new PublicKey(
|
||||
'DPiH3H3c7t47BMxqTxLsuPQpEC6Kne8GA9VXbxpnZxFE',
|
||||
);
|
||||
export const MANGO_GOVERNANCE_PROGRAM = new PublicKey(
|
||||
'GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J',
|
||||
);
|
||||
|
||||
export const createProposal = async (
|
||||
connection: Connection,
|
||||
wallet: WalletSigner,
|
||||
governance: PublicKey,
|
||||
tokenOwnerRecord: ProgramAccount<TokenOwnerRecord>,
|
||||
name: string,
|
||||
descriptionLink: string,
|
||||
proposalIndex: number,
|
||||
proposalInstructions: TransactionInstruction[],
|
||||
client: VsrClient,
|
||||
) => {
|
||||
const instructions: TransactionInstruction[] = [];
|
||||
const walletPk = wallet.publicKey!;
|
||||
const governanceAuthority = walletPk;
|
||||
const signatory = walletPk;
|
||||
const payer = walletPk;
|
||||
|
||||
// Changed this because it is misbehaving on my local validator setup.
|
||||
const programVersion = await getGovernanceProgramVersion(
|
||||
connection,
|
||||
MANGO_GOVERNANCE_PROGRAM,
|
||||
);
|
||||
|
||||
// V2 Approve/Deny configuration
|
||||
const voteType = VoteType.SINGLE_CHOICE;
|
||||
const options = ['Approve'];
|
||||
const useDenyOption = true;
|
||||
|
||||
const { updateVoterWeightRecordIx, voterWeightPk } =
|
||||
await updateVoterWeightRecord(
|
||||
client,
|
||||
tokenOwnerRecord.account.governingTokenOwner,
|
||||
);
|
||||
instructions.push(updateVoterWeightRecordIx);
|
||||
|
||||
const proposalAddress = await withCreateProposal(
|
||||
instructions,
|
||||
MANGO_GOVERNANCE_PROGRAM,
|
||||
programVersion,
|
||||
MANGO_REALM_PK,
|
||||
governance,
|
||||
tokenOwnerRecord.pubkey,
|
||||
name,
|
||||
descriptionLink,
|
||||
new PublicKey(MANGO_MINT),
|
||||
governanceAuthority,
|
||||
proposalIndex,
|
||||
voteType,
|
||||
options,
|
||||
useDenyOption,
|
||||
payer,
|
||||
voterWeightPk,
|
||||
);
|
||||
|
||||
await withAddSignatory(
|
||||
instructions,
|
||||
MANGO_GOVERNANCE_PROGRAM,
|
||||
programVersion,
|
||||
proposalAddress,
|
||||
tokenOwnerRecord.pubkey,
|
||||
governanceAuthority,
|
||||
signatory,
|
||||
payer,
|
||||
);
|
||||
|
||||
const signatoryRecordAddress = await getSignatoryRecordAddress(
|
||||
MANGO_GOVERNANCE_PROGRAM,
|
||||
proposalAddress,
|
||||
signatory,
|
||||
);
|
||||
const insertInstructions: TransactionInstruction[] = [];
|
||||
for (const i in proposalInstructions) {
|
||||
const instruction = getInstructionDataFromBase64(
|
||||
serializeInstructionToBase64(proposalInstructions[i]),
|
||||
);
|
||||
await withInsertTransaction(
|
||||
insertInstructions,
|
||||
MANGO_GOVERNANCE_PROGRAM,
|
||||
programVersion,
|
||||
governance,
|
||||
proposalAddress,
|
||||
tokenOwnerRecord.pubkey,
|
||||
governanceAuthority,
|
||||
Number(i),
|
||||
0,
|
||||
0,
|
||||
[instruction],
|
||||
payer,
|
||||
);
|
||||
}
|
||||
withSignOffProposal(
|
||||
insertInstructions, // SingOff proposal needs to be executed after inserting instructions hence we add it to insertInstructions
|
||||
MANGO_GOVERNANCE_PROGRAM,
|
||||
programVersion,
|
||||
MANGO_REALM_PK,
|
||||
governance,
|
||||
proposalAddress,
|
||||
signatory,
|
||||
signatoryRecordAddress,
|
||||
undefined,
|
||||
);
|
||||
|
||||
const txChunks = chunk([...instructions, ...insertInstructions], 2);
|
||||
|
||||
const transactions: Transaction[] = [];
|
||||
const latestBlockhash = await connection.getLatestBlockhash('confirmed');
|
||||
for (const chunk of txChunks) {
|
||||
const tx = new Transaction();
|
||||
tx.add(...chunk);
|
||||
tx.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
|
||||
tx.recentBlockhash = latestBlockhash.blockhash;
|
||||
tx.feePayer = payer;
|
||||
transactions.push(tx);
|
||||
}
|
||||
|
||||
const signedTransactions = await wallet.signAllTransactions(transactions);
|
||||
for (const tx of signedTransactions) {
|
||||
const rawTransaction = tx.serialize();
|
||||
const address = await connection.sendRawTransaction(rawTransaction, {
|
||||
skipPreflight: true,
|
||||
});
|
||||
await connection.confirmTransaction({
|
||||
blockhash: latestBlockhash.blockhash,
|
||||
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
||||
signature: address,
|
||||
});
|
||||
}
|
||||
return proposalAddress;
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
|
||||
import { SYSTEM_PROGRAM_ID } from '@solana/spl-governance';
|
||||
import { MANGO_MINT, MANGO_REALM_PK } from './constants';
|
||||
import { VsrClient, DEFAULT_VSR_ID } from './voteStakeRegistryClient';
|
||||
import { getRegistrarPDA, getVoterPDA, getVoterWeightPDA } from './vsrAccounts';
|
||||
|
||||
export const updateVoterWeightRecord = async (
|
||||
client: VsrClient,
|
||||
walletPk: PublicKey,
|
||||
) => {
|
||||
const { registrar } = await getRegistrarPDA(
|
||||
MANGO_REALM_PK,
|
||||
new PublicKey(MANGO_MINT),
|
||||
DEFAULT_VSR_ID,
|
||||
);
|
||||
const { voter } = await getVoterPDA(registrar, walletPk, DEFAULT_VSR_ID);
|
||||
const { voterWeightPk } = await getVoterWeightPDA(
|
||||
registrar,
|
||||
walletPk,
|
||||
DEFAULT_VSR_ID,
|
||||
);
|
||||
const updateVoterWeightRecordIx = await client!.program.methods
|
||||
.updateVoterWeightRecord()
|
||||
.accounts({
|
||||
registrar,
|
||||
voter,
|
||||
voterWeightRecord: voterWeightPk,
|
||||
systemProgram: SYSTEM_PROGRAM_ID,
|
||||
})
|
||||
.instruction();
|
||||
return { updateVoterWeightRecordIx, voterWeightPk };
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import { Program, Provider, web3 } from '@coral-xyz/anchor';
|
||||
import { Idl } from '@coral-xyz/anchor';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
|
||||
export const DEFAULT_VSR_ID = new web3.PublicKey(
|
||||
'4Q6WW2ouZ6V3iaNm56MTd5n2tnTm4C5fiH8miFHnAFHo',
|
||||
);
|
||||
|
||||
export class VsrClient {
|
||||
constructor(public program: Program<Idl>, public devnet?: boolean) {}
|
||||
|
||||
static async connect(
|
||||
provider: Provider,
|
||||
programId: web3.PublicKey,
|
||||
devnet?: boolean,
|
||||
): Promise<VsrClient> {
|
||||
const idl = await Program.fetchIdl(new PublicKey(DEFAULT_VSR_ID), provider);
|
||||
return new VsrClient(
|
||||
new Program<Idl>(idl as Idl, programId, provider),
|
||||
devnet,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
import { BN } from '@coral-xyz/anchor';
|
||||
import { Mint } from '@solana/spl-token';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { VsrClient } from './voteStakeRegistryClient';
|
||||
|
||||
export type TokenProgramAccount<T> = {
|
||||
publicKey: PublicKey;
|
||||
account: T;
|
||||
};
|
||||
|
||||
export interface Voter {
|
||||
deposits: Deposit[];
|
||||
voterAuthority: PublicKey;
|
||||
registrar: PublicKey;
|
||||
//there are more fields but no use for them on ui yet
|
||||
}
|
||||
|
||||
export interface votingMint {
|
||||
baselineVoteWeightScaledFactor: BN;
|
||||
digitShift: number;
|
||||
grantAuthority: PublicKey;
|
||||
lockupSaturationSecs: BN;
|
||||
maxExtraLockupVoteWeightScaledFactor: BN;
|
||||
mint: PublicKey;
|
||||
}
|
||||
|
||||
export type LockupType = 'none' | 'monthly' | 'cliff' | 'constant' | 'daily';
|
||||
export interface Registrar {
|
||||
governanceProgramId: PublicKey;
|
||||
realm: PublicKey;
|
||||
realmAuthority: PublicKey;
|
||||
realmGoverningTokenMint: PublicKey;
|
||||
votingMints: votingMint[];
|
||||
//there are more fields but no use for them on ui yet
|
||||
}
|
||||
interface LockupKind {
|
||||
none: object;
|
||||
daily: object;
|
||||
monthly: object;
|
||||
cliff: object;
|
||||
constant: object;
|
||||
}
|
||||
interface Lockup {
|
||||
endTs: BN;
|
||||
kind: LockupKind;
|
||||
startTs: BN;
|
||||
}
|
||||
export interface Deposit {
|
||||
allowClawback: boolean;
|
||||
amountDepositedNative: BN;
|
||||
amountInitiallyLockedNative: BN;
|
||||
isUsed: boolean;
|
||||
lockup: Lockup;
|
||||
votingMintConfigIdx: number;
|
||||
}
|
||||
|
||||
export interface DepositWithMintAccount extends Deposit {
|
||||
mint: TokenProgramAccount<Mint>;
|
||||
index: number;
|
||||
available: BN;
|
||||
vestingRate: BN | null;
|
||||
currentlyLocked: BN;
|
||||
nextVestingTimestamp: BN | null;
|
||||
votingPower: BN;
|
||||
votingPowerBaseline: BN;
|
||||
}
|
||||
|
||||
export const emptyPk = '11111111111111111111111111111111';
|
||||
|
||||
export const getRegistrarPDA = async (
|
||||
realmPk: PublicKey,
|
||||
mint: PublicKey,
|
||||
clientProgramId: PublicKey,
|
||||
) => {
|
||||
const [registrar, registrarBump] = await PublicKey.findProgramAddress(
|
||||
[realmPk.toBuffer(), Buffer.from('registrar'), mint.toBuffer()],
|
||||
clientProgramId,
|
||||
);
|
||||
return {
|
||||
registrar,
|
||||
registrarBump,
|
||||
};
|
||||
};
|
||||
|
||||
export const getVoterPDA = async (
|
||||
registrar: PublicKey,
|
||||
walletPk: PublicKey,
|
||||
clientProgramId: PublicKey,
|
||||
) => {
|
||||
const [voter, voterBump] = await PublicKey.findProgramAddress(
|
||||
[registrar.toBuffer(), Buffer.from('voter'), walletPk.toBuffer()],
|
||||
clientProgramId,
|
||||
);
|
||||
|
||||
return {
|
||||
voter,
|
||||
voterBump,
|
||||
};
|
||||
};
|
||||
|
||||
export const getVoterWeightPDA = async (
|
||||
registrar: PublicKey,
|
||||
walletPk: PublicKey,
|
||||
clientProgramId: PublicKey,
|
||||
) => {
|
||||
const [voterWeightPk, voterWeightBump] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
registrar.toBuffer(),
|
||||
Buffer.from('voter-weight-record'),
|
||||
walletPk.toBuffer(),
|
||||
],
|
||||
clientProgramId,
|
||||
);
|
||||
|
||||
return {
|
||||
voterWeightPk,
|
||||
voterWeightBump,
|
||||
};
|
||||
};
|
||||
|
||||
export const tryGetVoter = async (voterPk: PublicKey, client: VsrClient) => {
|
||||
try {
|
||||
const voter = await client?.program.account.voter.fetch(voterPk);
|
||||
return voter as unknown as Voter;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
export const tryGetRegistrar = async (
|
||||
registrarPk: PublicKey,
|
||||
client: VsrClient,
|
||||
) => {
|
||||
try {
|
||||
const existingRegistrar = await client.program.account.registrar.fetch(
|
||||
registrarPk,
|
||||
);
|
||||
return existingRegistrar as unknown as Registrar;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -606,7 +606,7 @@ async function makeMarketUpdateInstructions(
|
|||
);
|
||||
moveOrders = openOrders.length < 2 || openOrders.length > 2;
|
||||
for (const o of openOrders) {
|
||||
const refPrice = o.side === 'buy' ? bookAdjBid : bookAdjAsk;
|
||||
const refPrice = o.side === PerpOrderSide.bid ? bookAdjBid : bookAdjAsk;
|
||||
moveOrders =
|
||||
moveOrders ||
|
||||
Math.abs(o.priceLots.toNumber() / refPrice.toNumber() - 1) >
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
import { Bank } from '../src/accounts/bank';
|
||||
import { Group } from '../src/accounts/group';
|
||||
import { isSwitchboardOracle } from '../src/accounts/oracle';
|
||||
import { PerpMarket } from '../src/accounts/perp';
|
||||
import { MangoClient } from '../src/client';
|
||||
import { buildFetch } from '../src/utils';
|
||||
|
||||
function getNameForBank(group: Group, oracle: PublicKey): string {
|
||||
let match: any[] = Array.from(group.banksMapByName.values())
|
||||
function getBankForOracle(group: Group, oracle: PublicKey): Bank | PerpMarket {
|
||||
let match: Bank[] | PerpMarket[] = Array.from(group.banksMapByName.values())
|
||||
.flat()
|
||||
.filter((b) => b.oracle.equals(oracle));
|
||||
if (match.length > 0) {
|
||||
return match[0].name;
|
||||
return match[0];
|
||||
}
|
||||
|
||||
match = Array.from(group.perpMarketsMapByName.values()).filter((p) =>
|
||||
p.oracle.equals(oracle),
|
||||
);
|
||||
if (match.length > 0) {
|
||||
return match[0].name;
|
||||
return match[0];
|
||||
}
|
||||
|
||||
throw new Error(`No token or perp market found for ${oracle}`);
|
||||
|
@ -57,7 +59,8 @@ async function main(): Promise<void> {
|
|||
method: 'POST',
|
||||
});
|
||||
|
||||
console.log(`${getNameForBank(group, o)} ${o}`);
|
||||
const bOrPm = getBankForOracle(group, o);
|
||||
console.log(`${bOrPm.name} ${bOrPm.oracleLastUpdatedSlot} ${o}`);
|
||||
|
||||
(await r.json()).forEach((e: { message: string; timestamp: string }) => {
|
||||
if (e.message.toLowerCase().includes('error')) {
|
||||
|
|
|
@ -0,0 +1,291 @@
|
|||
import {
|
||||
LISTING_PRESETS,
|
||||
LISTING_PRESETS_PYTH,
|
||||
MidPriceImpact,
|
||||
getMidPriceImpacts,
|
||||
getProposedTier,
|
||||
} from '@blockworks-foundation/mango-v4-settings/lib/helpers/listingTools';
|
||||
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
||||
import { BN } from '@project-serum/anchor';
|
||||
import {
|
||||
getAllProposals,
|
||||
getTokenOwnerRecord,
|
||||
getTokenOwnerRecordAddress,
|
||||
} from '@solana/spl-governance';
|
||||
import {
|
||||
AccountMeta,
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { OracleProvider } from '../src/accounts/oracle';
|
||||
import { Builder } from '../src/builder';
|
||||
import { MangoClient } from '../src/client';
|
||||
import { NullTokenEditParams } from '../src/clientIxParamBuilder';
|
||||
import { MANGO_V4_MAIN_GROUP as MANGO_V4_PRIMARY_GROUP } from '../src/constants';
|
||||
import { toUiDecimalsForQuote } from '../src/utils';
|
||||
import {
|
||||
MANGO_DAO_WALLET_GOVERNANCE,
|
||||
MANGO_GOVERNANCE_PROGRAM,
|
||||
MANGO_MINT,
|
||||
MANGO_REALM_PK,
|
||||
} from './governanceInstructions/constants';
|
||||
import { createProposal } from './governanceInstructions/createProposal';
|
||||
import {
|
||||
DEFAULT_VSR_ID,
|
||||
VsrClient,
|
||||
} from './governanceInstructions/voteStakeRegistryClient';
|
||||
|
||||
const {
|
||||
MB_CLUSTER_URL,
|
||||
PROPOSAL_TITLE,
|
||||
VSR_DELEGATE_KEYPAIR,
|
||||
VSR_DELEGATE_FROM_PK,
|
||||
DRY_RUN,
|
||||
} = process.env;
|
||||
|
||||
const getApiTokenName = (bankName: string) => {
|
||||
if (bankName === 'ETH (Portal)') {
|
||||
return 'ETH';
|
||||
}
|
||||
return bankName;
|
||||
};
|
||||
|
||||
async function buildClient(): Promise<MangoClient> {
|
||||
return await MangoClient.connectDefault(MB_CLUSTER_URL!);
|
||||
}
|
||||
|
||||
async function setupWallet(): Promise<Wallet> {
|
||||
const clientKeypair = Keypair.fromSecretKey(
|
||||
Buffer.from(JSON.parse(fs.readFileSync(VSR_DELEGATE_KEYPAIR!, 'utf-8'))),
|
||||
);
|
||||
const clientWallet = new Wallet(clientKeypair);
|
||||
|
||||
return clientWallet;
|
||||
}
|
||||
|
||||
async function setupVsr(
|
||||
connection: Connection,
|
||||
clientWallet: Wallet,
|
||||
): Promise<VsrClient> {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const provider = new AnchorProvider(connection, clientWallet, options);
|
||||
const vsrClient = await VsrClient.connect(provider, DEFAULT_VSR_ID);
|
||||
return vsrClient;
|
||||
}
|
||||
|
||||
async function updateTokenParams(): Promise<void> {
|
||||
const [client, wallet] = await Promise.all([buildClient(), setupWallet()]);
|
||||
const vsrClient = await setupVsr(client.connection, wallet);
|
||||
|
||||
const group = await client.getGroup(MANGO_V4_PRIMARY_GROUP);
|
||||
|
||||
const instructions: TransactionInstruction[] = [];
|
||||
const midPriceImpacts = getMidPriceImpacts(group.pis);
|
||||
|
||||
Array.from(group.banksMapByTokenIndex.values())
|
||||
.map((banks) => banks[0])
|
||||
.filter(
|
||||
(bank) =>
|
||||
bank.mint.toBase58() == 'So11111111111111111111111111111111111111112' ||
|
||||
bank.name.toLocaleLowerCase().indexOf('usdc') > -1 ||
|
||||
bank.name.toLocaleLowerCase().indexOf('stsol') > -1,
|
||||
)
|
||||
.forEach(async (bank) => {
|
||||
// Limit borrows to 1/3rd of deposit, rounded to 1000, only update if more than 10% different
|
||||
const depositsInUsd = bank.nativeDeposits().mul(bank.price);
|
||||
let newNetBorrowLimitPerWindowQuote: number | null =
|
||||
depositsInUsd.toNumber() / 3;
|
||||
newNetBorrowLimitPerWindowQuote =
|
||||
Math.round(newNetBorrowLimitPerWindowQuote / 1_000_000_000) *
|
||||
1_000_000_000;
|
||||
newNetBorrowLimitPerWindowQuote =
|
||||
Math.abs(
|
||||
(newNetBorrowLimitPerWindowQuote -
|
||||
bank.netBorrowLimitPerWindowQuote.toNumber()) /
|
||||
bank.netBorrowLimitPerWindowQuote.toNumber(),
|
||||
) > 0.1
|
||||
? newNetBorrowLimitPerWindowQuote
|
||||
: null;
|
||||
|
||||
// Kick in weight scaling as late as possible until liquidation fee remains reasonable
|
||||
// Only update if more than 10% different
|
||||
let newWeightScaleQuote: number | null = null;
|
||||
if (
|
||||
bank.tokenIndex != 0 && // USDC
|
||||
bank.mint.toBase58() != 'So11111111111111111111111111111111111111112' // SOL
|
||||
) {
|
||||
const PRESETS =
|
||||
bank?.oracleProvider === OracleProvider.Pyth
|
||||
? LISTING_PRESETS_PYTH
|
||||
: LISTING_PRESETS;
|
||||
|
||||
const tokenToPriceImpact = midPriceImpacts
|
||||
.filter((x) => x.avg_price_impact_percent < 1)
|
||||
.reduce(
|
||||
(acc: { [key: string]: MidPriceImpact }, val: MidPriceImpact) => {
|
||||
if (
|
||||
!acc[val.symbol] ||
|
||||
val.target_amount > acc[val.symbol].target_amount
|
||||
) {
|
||||
acc[val.symbol] = val;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const priceImpact = tokenToPriceImpact[getApiTokenName(bank.name)];
|
||||
const suggestedTier = getProposedTier(
|
||||
PRESETS,
|
||||
priceImpact?.target_amount,
|
||||
bank.oracleProvider === OracleProvider.Pyth,
|
||||
);
|
||||
newWeightScaleQuote =
|
||||
PRESETS[suggestedTier].borrowWeightScaleStartQuote;
|
||||
|
||||
newWeightScaleQuote =
|
||||
bank.depositWeightScaleStartQuote !== newWeightScaleQuote ||
|
||||
bank.borrowWeightScaleStartQuote !== newWeightScaleQuote
|
||||
? newWeightScaleQuote
|
||||
: null;
|
||||
}
|
||||
|
||||
if (
|
||||
newNetBorrowLimitPerWindowQuote == null &&
|
||||
newWeightScaleQuote == null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = Builder(NullTokenEditParams)
|
||||
.netBorrowLimitPerWindowQuote(newNetBorrowLimitPerWindowQuote)
|
||||
.borrowWeightScaleStartQuote(newWeightScaleQuote)
|
||||
.depositWeightScaleStartQuote(newWeightScaleQuote)
|
||||
.build();
|
||||
|
||||
const ix = await client.program.methods
|
||||
.tokenEdit(
|
||||
params.oracle,
|
||||
params.oracleConfig,
|
||||
params.groupInsuranceFund,
|
||||
params.interestRateParams,
|
||||
params.loanFeeRate,
|
||||
params.loanOriginationFeeRate,
|
||||
params.maintAssetWeight,
|
||||
params.initAssetWeight,
|
||||
params.maintLiabWeight,
|
||||
params.initLiabWeight,
|
||||
params.liquidationFee,
|
||||
params.stablePriceDelayIntervalSeconds,
|
||||
params.stablePriceDelayGrowthLimit,
|
||||
params.stablePriceGrowthLimit,
|
||||
params.minVaultToDepositsRatio,
|
||||
params.netBorrowLimitPerWindowQuote !== null
|
||||
? new BN(params.netBorrowLimitPerWindowQuote)
|
||||
: null,
|
||||
params.netBorrowLimitWindowSizeTs !== null
|
||||
? new BN(params.netBorrowLimitWindowSizeTs)
|
||||
: null,
|
||||
params.borrowWeightScaleStartQuote,
|
||||
params.depositWeightScaleStartQuote,
|
||||
params.resetStablePrice ?? false,
|
||||
params.resetNetBorrowLimit ?? false,
|
||||
params.reduceOnly,
|
||||
params.name,
|
||||
params.forceClose,
|
||||
params.tokenConditionalSwapTakerFeeRate,
|
||||
params.tokenConditionalSwapMakerFeeRate,
|
||||
params.flashLoanDepositFeeRate,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
oracle: bank.oracle,
|
||||
admin: group.admin,
|
||||
mintInfo: group.mintInfosMapByTokenIndex.get(bank.tokenIndex)
|
||||
?.publicKey,
|
||||
})
|
||||
.remainingAccounts([
|
||||
{
|
||||
pubkey: bank.publicKey,
|
||||
isWritable: true,
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
])
|
||||
.instruction();
|
||||
|
||||
const tx = new Transaction({ feePayer: wallet.publicKey }).add(ix);
|
||||
const simulated = await client.connection.simulateTransaction(tx);
|
||||
|
||||
if (simulated.value.err) {
|
||||
console.log('error', simulated.value.logs);
|
||||
throw simulated.value.logs;
|
||||
}
|
||||
|
||||
console.log(`Bank ${bank.name}`);
|
||||
console.log(
|
||||
`- netBorrowLimitPerWindowQuote UI old ${toUiDecimalsForQuote(
|
||||
bank.netBorrowLimitPerWindowQuote.toNumber(),
|
||||
).toLocaleString()} new ${toUiDecimalsForQuote(
|
||||
newNetBorrowLimitPerWindowQuote!,
|
||||
).toLocaleString()}`,
|
||||
);
|
||||
console.log(
|
||||
`- WeightScaleQuote UI old ${toUiDecimalsForQuote(
|
||||
bank.depositWeightScaleStartQuote,
|
||||
).toLocaleString()} new ${toUiDecimalsForQuote(
|
||||
newWeightScaleQuote!,
|
||||
).toLocaleString()}`,
|
||||
);
|
||||
instructions.push(ix);
|
||||
});
|
||||
|
||||
const tokenOwnerRecordPk = await getTokenOwnerRecordAddress(
|
||||
MANGO_GOVERNANCE_PROGRAM,
|
||||
MANGO_REALM_PK,
|
||||
MANGO_MINT,
|
||||
new PublicKey(VSR_DELEGATE_FROM_PK!),
|
||||
);
|
||||
|
||||
const [tokenOwnerRecord, proposals] = await Promise.all([
|
||||
getTokenOwnerRecord(client.connection, tokenOwnerRecordPk),
|
||||
getAllProposals(
|
||||
client.connection,
|
||||
MANGO_GOVERNANCE_PROGRAM,
|
||||
MANGO_REALM_PK,
|
||||
),
|
||||
]);
|
||||
|
||||
const walletSigner = wallet as never;
|
||||
|
||||
if (!DRY_RUN) {
|
||||
const proposalAddress = await createProposal(
|
||||
client.connection,
|
||||
walletSigner,
|
||||
MANGO_DAO_WALLET_GOVERNANCE,
|
||||
tokenOwnerRecord,
|
||||
PROPOSAL_TITLE ? PROPOSAL_TITLE : 'Update risk parameters for tokens',
|
||||
'',
|
||||
Object.values(proposals).length,
|
||||
instructions,
|
||||
vsrClient!,
|
||||
);
|
||||
console.log(proposalAddress.toBase58());
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
await updateTokenParams();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
|
@ -3,7 +3,7 @@ import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes';
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||
import { As, toUiDecimals } from '../utils';
|
||||
import { OracleProvider } from './oracle';
|
||||
import { OracleProvider, isOracleStaleOrUnconfident } from './oracle';
|
||||
|
||||
export type TokenIndex = number & As<'token-index'>;
|
||||
|
||||
|
@ -64,6 +64,7 @@ export class Bank implements BankForHealth {
|
|||
public _price: I80F48 | undefined;
|
||||
public _uiPrice: number | undefined;
|
||||
public _oracleLastUpdatedSlot: number | undefined;
|
||||
public _oracleLastKnownDeviation: I80F48 | undefined;
|
||||
public _oracleProvider: OracleProvider | undefined;
|
||||
public collectedFeesNative: I80F48;
|
||||
public loanFeeRate: I80F48;
|
||||
|
@ -330,6 +331,17 @@ export class Bank implements BankForHealth {
|
|||
);
|
||||
}
|
||||
|
||||
isOracleStaleOrUnconfident(nowSlot: number): boolean {
|
||||
return isOracleStaleOrUnconfident(
|
||||
nowSlot,
|
||||
this.oracleConfig.maxStalenessSlots.toNumber(),
|
||||
this.oracleLastUpdatedSlot,
|
||||
this._oracleLastKnownDeviation,
|
||||
this.oracleConfig.confFilter,
|
||||
this.price,
|
||||
);
|
||||
}
|
||||
|
||||
areDepositsReduceOnly(): boolean {
|
||||
return this.reduceOnly == 1;
|
||||
}
|
||||
|
@ -533,9 +545,10 @@ export class Bank implements BankForHealth {
|
|||
}
|
||||
|
||||
getTimeToNextBorrowLimitWindowStartsTs(): number {
|
||||
return this.netBorrowLimitWindowSizeTs
|
||||
const timeToNextBorrowLimitWindowStartsTs = this.netBorrowLimitWindowSizeTs
|
||||
.sub(new BN(Date.now() / 1000).sub(this.lastNetBorrowsWindowStartTs))
|
||||
.toNumber();
|
||||
return Math.max(timeToNextBorrowLimitWindowStartsTs, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import merge from 'lodash/merge';
|
|||
import { MangoClient } from '../client';
|
||||
import { OPENBOOK_PROGRAM_ID } from '../constants';
|
||||
import { Id } from '../ids';
|
||||
import { I80F48, ONE_I80F48 } from '../numbers/I80F48';
|
||||
import { I80F48 } from '../numbers/I80F48';
|
||||
import { PriceImpact, computePriceImpactOnJup } from '../risk';
|
||||
import { buildFetch, toNative, toNativeI80F48, toUiDecimals } from '../utils';
|
||||
import { Bank, MintInfo, TokenIndex } from './bank';
|
||||
|
@ -389,27 +389,23 @@ export class Group {
|
|||
const coder = new BorshAccountsCoder(client.program.idl);
|
||||
for (const [index, ai] of ais.entries()) {
|
||||
for (const bank of banks[index]) {
|
||||
if (bank.name === 'USDC') {
|
||||
bank._price = ONE_I80F48();
|
||||
bank._uiPrice = 1;
|
||||
} else {
|
||||
if (!ai)
|
||||
throw new Error(
|
||||
`Undefined accountInfo object in reloadBankOraclePrices for ${bank.oracle}!`,
|
||||
);
|
||||
const { price, uiPrice, lastUpdatedSlot, provider } =
|
||||
await this.decodePriceFromOracleAi(
|
||||
coder,
|
||||
bank.oracle,
|
||||
ai,
|
||||
this.getMintDecimals(bank.mint),
|
||||
client,
|
||||
);
|
||||
bank._price = price;
|
||||
bank._uiPrice = uiPrice;
|
||||
bank._oracleLastUpdatedSlot = lastUpdatedSlot;
|
||||
bank._oracleProvider = provider;
|
||||
}
|
||||
if (!ai)
|
||||
throw new Error(
|
||||
`Undefined accountInfo object in reloadBankOraclePrices for ${bank.oracle}!`,
|
||||
);
|
||||
const { price, uiPrice, lastUpdatedSlot, provider, deviation } =
|
||||
await this.decodePriceFromOracleAi(
|
||||
coder,
|
||||
bank.oracle,
|
||||
ai,
|
||||
this.getMintDecimals(bank.mint),
|
||||
client,
|
||||
);
|
||||
bank._price = price;
|
||||
bank._uiPrice = uiPrice;
|
||||
bank._oracleLastUpdatedSlot = lastUpdatedSlot;
|
||||
bank._oracleProvider = provider;
|
||||
bank._oracleLastKnownDeviation = deviation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -433,7 +429,7 @@ export class Group {
|
|||
`Undefined ai object in reloadPerpMarketOraclePrices for ${perpMarket.oracle}!`,
|
||||
);
|
||||
|
||||
const { price, uiPrice, lastUpdatedSlot, provider } =
|
||||
const { price, uiPrice, lastUpdatedSlot, provider, deviation } =
|
||||
await this.decodePriceFromOracleAi(
|
||||
coder,
|
||||
perpMarket.oracle,
|
||||
|
@ -445,6 +441,7 @@ export class Group {
|
|||
perpMarket._uiPrice = uiPrice;
|
||||
perpMarket._oracleLastUpdatedSlot = lastUpdatedSlot;
|
||||
perpMarket._oracleProvider = provider;
|
||||
perpMarket._oracleLastKnownDeviation = deviation;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -460,8 +457,9 @@ export class Group {
|
|||
uiPrice: number;
|
||||
lastUpdatedSlot: number;
|
||||
provider: OracleProvider;
|
||||
deviation: I80F48;
|
||||
}> {
|
||||
let price, uiPrice, lastUpdatedSlot, provider;
|
||||
let price, uiPrice, lastUpdatedSlot, provider, deviation;
|
||||
if (
|
||||
!BorshAccountsCoder.accountDiscriminator('stubOracle').compare(
|
||||
ai.data.slice(0, 8),
|
||||
|
@ -472,11 +470,17 @@ export class Group {
|
|||
uiPrice = this.toUiPrice(price, baseDecimals);
|
||||
lastUpdatedSlot = stubOracle.lastUpdateSlot.toNumber();
|
||||
provider = OracleProvider.Stub;
|
||||
deviation = stubOracle.deviation;
|
||||
} else if (isPythOracle(ai)) {
|
||||
const priceData = parsePriceData(ai.data);
|
||||
uiPrice = priceData.previousPrice;
|
||||
price = this.toNativePrice(uiPrice, baseDecimals);
|
||||
lastUpdatedSlot = parseInt(priceData.lastSlot.toString());
|
||||
deviation =
|
||||
priceData.previousConfidence !== undefined
|
||||
? this.toNativePrice(priceData.previousConfidence, baseDecimals)
|
||||
: undefined;
|
||||
|
||||
provider = OracleProvider.Pyth;
|
||||
} else if (isSwitchboardOracle(ai)) {
|
||||
const priceData = await parseSwitchboardOracle(
|
||||
|
@ -486,13 +490,14 @@ export class Group {
|
|||
uiPrice = priceData.price;
|
||||
price = this.toNativePrice(uiPrice, baseDecimals);
|
||||
lastUpdatedSlot = priceData.lastUpdatedSlot;
|
||||
deviation = this.toNativePrice(priceData.uiDeviation, baseDecimals);
|
||||
provider = OracleProvider.Switchboard;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unknown oracle provider (parsing not implemented) for oracle ${oracle}, with owner ${ai.owner}!`,
|
||||
);
|
||||
}
|
||||
return { price, uiPrice, lastUpdatedSlot, provider };
|
||||
return { price, uiPrice, lastUpdatedSlot, provider, deviation };
|
||||
}
|
||||
|
||||
public async reloadVaults(client: MangoClient): Promise<void> {
|
||||
|
|
|
@ -4,10 +4,11 @@ import { expect } from 'chai';
|
|||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import range from 'lodash/range';
|
||||
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||
import { BankForHealth, StablePriceModel, TokenIndex } from './bank';
|
||||
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
|
||||
import { HealthType, PerpPosition } from './mangoAccount';
|
||||
import { HealthType, PerpPosition, Serum3Orders } from './mangoAccount';
|
||||
import { PerpMarket, PerpOrderSide } from './perp';
|
||||
import { MarketIndex } from './serum3';
|
||||
|
||||
|
@ -108,6 +109,14 @@ describe('Health Cache', () => {
|
|||
const ti2 = TokenInfo.fromBank(targetBank, I80F48.fromNumber(-10));
|
||||
|
||||
const si1 = Serum3Info.fromOoModifyingTokenInfos(
|
||||
new Serum3Orders(
|
||||
PublicKey.default,
|
||||
2 as MarketIndex,
|
||||
4 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
1,
|
||||
ti2,
|
||||
0,
|
||||
|
@ -185,6 +194,8 @@ describe('Health Cache', () => {
|
|||
bs3: [number, number];
|
||||
oo12: [number, number];
|
||||
oo13: [number, number];
|
||||
sa12: [number, number];
|
||||
sa13: [number, number];
|
||||
perp1: [number, number, number, number];
|
||||
expectedHealth: number;
|
||||
}): void {
|
||||
|
@ -228,6 +239,14 @@ describe('Health Cache', () => {
|
|||
const ti3 = TokenInfo.fromBank(bank3, I80F48.fromNumber(fixture.token3));
|
||||
|
||||
const si1 = Serum3Info.fromOoModifyingTokenInfos(
|
||||
new Serum3Orders(
|
||||
PublicKey.default,
|
||||
2 as MarketIndex,
|
||||
4 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
fixture.sa12[0],
|
||||
fixture.sa12[1],
|
||||
),
|
||||
1,
|
||||
ti2,
|
||||
0,
|
||||
|
@ -243,11 +262,19 @@ describe('Health Cache', () => {
|
|||
);
|
||||
|
||||
const si2 = Serum3Info.fromOoModifyingTokenInfos(
|
||||
new Serum3Orders(
|
||||
PublicKey.default,
|
||||
3 as MarketIndex,
|
||||
5 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
fixture.sa13[0],
|
||||
fixture.sa13[1],
|
||||
),
|
||||
2,
|
||||
ti3,
|
||||
0,
|
||||
ti1,
|
||||
2 as MarketIndex,
|
||||
3 as MarketIndex,
|
||||
{
|
||||
quoteTokenTotal: new BN(fixture.oo13[0]),
|
||||
baseTokenTotal: new BN(fixture.oo13[1]),
|
||||
|
@ -291,7 +318,7 @@ describe('Health Cache', () => {
|
|||
.toFixed(3)
|
||||
.padStart(10)}, expected ${fixture.expectedHealth}`,
|
||||
);
|
||||
expect(health - fixture.expectedHealth).lessThan(0.0000001);
|
||||
expect(Math.abs(health - fixture.expectedHealth)).lessThan(0.0000001);
|
||||
}
|
||||
|
||||
const basePrice = 5;
|
||||
|
@ -307,6 +334,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [20, 15],
|
||||
oo13: [0, 0],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [3, -131, 7, 11],
|
||||
expectedHealth:
|
||||
// for token1
|
||||
|
@ -331,6 +360,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [20, 15],
|
||||
oo13: [0, 0],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [-10, -131, 7, 11],
|
||||
expectedHealth:
|
||||
// for token1
|
||||
|
@ -353,6 +384,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [-1, 100, 0, 0],
|
||||
expectedHealth: 0.8 * 0.95 * (100.0 - 1.2 * 1.0 * baseLotsToQuote),
|
||||
});
|
||||
|
@ -367,6 +400,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [1, -100, 0, 0],
|
||||
expectedHealth: 1.2 * (-100.0 + 0.8 * 1.0 * baseLotsToQuote),
|
||||
});
|
||||
|
@ -381,6 +416,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [10, 100, 0, 0],
|
||||
expectedHealth: 0.8 * 0.95 * (100.0 + 0.8 * 10.0 * baseLotsToQuote),
|
||||
});
|
||||
|
@ -395,6 +432,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [30, -100, 0, 0],
|
||||
expectedHealth: 0.8 * 0.95 * (-100.0 + 0.8 * 30.0 * baseLotsToQuote),
|
||||
});
|
||||
|
@ -409,6 +448,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [1, 1],
|
||||
oo13: [1, 1],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
// tokens
|
||||
|
@ -431,6 +472,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [1, 1],
|
||||
oo13: [1, 1],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
-14.0 * 1.2 -
|
||||
|
@ -454,6 +497,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [0, 0],
|
||||
oo13: [10, 1],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
// tokens
|
||||
|
@ -475,6 +520,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [100, 0],
|
||||
oo13: [10, 1],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
// tokens
|
||||
|
@ -498,6 +545,8 @@ describe('Health Cache', () => {
|
|||
bs3: [10000, 10000],
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
// token1
|
||||
|
@ -518,6 +567,8 @@ describe('Health Cache', () => {
|
|||
bs3: [10000, 10000],
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
// token1
|
||||
|
@ -538,6 +589,8 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [1, 100, 0, 0],
|
||||
expectedHealth:
|
||||
0.8 * (-100.0 + 0.95 * (100.0 + 0.8 * 1.0 * baseLotsToQuote)),
|
||||
|
@ -553,10 +606,58 @@ describe('Health Cache', () => {
|
|||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
sa12: [0, 0],
|
||||
sa13: [0, 0],
|
||||
perp1: [-1, -100, 0, 0],
|
||||
expectedHealth: 1.2 * (100.0 - 100.0 - 1.2 * 1.0 * baseLotsToQuote),
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '14, reserved oo funds with max bid/min ask',
|
||||
token1: -100,
|
||||
token2: -10,
|
||||
token3: 0,
|
||||
bs1: [0, Number.MAX_SAFE_INTEGER],
|
||||
bs2: [0, Number.MAX_SAFE_INTEGER],
|
||||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [1, 1],
|
||||
oo13: [11, 1],
|
||||
sa12: [0, 3],
|
||||
sa13: [1.0 / 12.0, 0],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
// tokens
|
||||
-100.0 * 1.2 -
|
||||
10.0 * 5.0 * 1.5 +
|
||||
// oo_1_2 (-> token1)
|
||||
(1.0 + 3.0) * 1.2 +
|
||||
// oo_1_3 (-> token3)
|
||||
(11.0 / 12.0 + 1.0) * 10.0 * 0.5,
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '15, reserved oo funds with max bid/min ask not crossing oracle',
|
||||
token1: -100,
|
||||
token2: -10,
|
||||
token3: 0,
|
||||
bs1: [0, Number.MAX_SAFE_INTEGER],
|
||||
bs2: [0, Number.MAX_SAFE_INTEGER],
|
||||
bs3: [0, Number.MAX_SAFE_INTEGER],
|
||||
oo12: [1, 1],
|
||||
oo13: [11, 1],
|
||||
sa12: [0, 6],
|
||||
sa13: [1.0 / 9.0, 0],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
// tokens
|
||||
-100.0 * 1.2 -
|
||||
10.0 * 5.0 * 1.5 +
|
||||
// oo_1_2 (-> token1)
|
||||
(1.0 + 5.0) * 1.2 +
|
||||
// oo_1_3 (-> token3)
|
||||
(11.0 / 10.0 + 1.0) * 10.0 * 0.5,
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -861,6 +962,8 @@ describe('Health Cache', () => {
|
|||
new Serum3Info(
|
||||
I80F48.fromNumber(30 / 3),
|
||||
I80F48.fromNumber(30 / 2),
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
1,
|
||||
0,
|
||||
0 as MarketIndex,
|
||||
|
|
|
@ -17,7 +17,12 @@ import {
|
|||
import { Bank, BankForHealth, TokenIndex } from './bank';
|
||||
import { Group } from './group';
|
||||
|
||||
import { HealthType, MangoAccount, PerpPosition } from './mangoAccount';
|
||||
import {
|
||||
HealthType,
|
||||
MangoAccount,
|
||||
PerpPosition,
|
||||
Serum3Orders,
|
||||
} from './mangoAccount';
|
||||
import { PerpMarket, PerpMarketIndex, PerpOrderSide } from './perp';
|
||||
import { MarketIndex, Serum3Market, Serum3Side } from './serum3';
|
||||
|
||||
|
@ -132,6 +137,7 @@ export class HealthCache {
|
|||
}
|
||||
|
||||
return Serum3Info.fromOoModifyingTokenInfos(
|
||||
serum3,
|
||||
baseInfoIndex,
|
||||
baseInfo,
|
||||
quoteInfoIndex,
|
||||
|
@ -182,14 +188,31 @@ export class HealthCache {
|
|||
|
||||
const quoteAsset = quote.prices.asset(healthType);
|
||||
const baseLiab = base.prices.liab(healthType);
|
||||
const allReservedAsBase = reservedBase.add(
|
||||
reservedQuote.mul(quoteAsset).div(baseLiab),
|
||||
const reservedQuoteAsBaseOracle = reservedQuote.mul(
|
||||
quoteAsset.div(baseLiab),
|
||||
);
|
||||
let allReservedAsBase;
|
||||
if (!info.reservedQuoteAsBaseHighestBid.eq(ZERO_I80F48())) {
|
||||
allReservedAsBase = reservedBase.add(
|
||||
reservedQuoteAsBaseOracle.min(info.reservedQuoteAsBaseHighestBid),
|
||||
);
|
||||
} else {
|
||||
allReservedAsBase = reservedBase.add(reservedQuoteAsBaseOracle);
|
||||
}
|
||||
|
||||
const baseAsset = base.prices.asset(healthType);
|
||||
const quoteLiab = quote.prices.liab(healthType);
|
||||
const allReservedAsQuote = reservedQuote.add(
|
||||
reservedBase.mul(baseAsset).div(quoteLiab),
|
||||
const reservedBaseAsQuoteOracle = reservedBase.mul(
|
||||
baseAsset.div(quoteLiab),
|
||||
);
|
||||
let allReservedAsQuote;
|
||||
if (!info.reservedBaseAsQuoteLowestAsk.eq(ZERO_I80F48())) {
|
||||
allReservedAsQuote = reservedQuote.add(
|
||||
reservedBaseAsQuoteOracle.min(info.reservedBaseAsQuoteLowestAsk),
|
||||
);
|
||||
} else {
|
||||
allReservedAsQuote = reservedQuote.add(reservedBaseAsQuoteOracle);
|
||||
}
|
||||
|
||||
const baseMaxReserved = tokenMaxReserved[info.baseInfoIndex];
|
||||
baseMaxReserved.maxSerumReserved.iadd(allReservedAsBase);
|
||||
|
@ -1531,6 +1554,8 @@ export class Serum3Info {
|
|||
constructor(
|
||||
public reservedBase: I80F48,
|
||||
public reservedQuote: I80F48,
|
||||
public reservedBaseAsQuoteLowestAsk: I80F48,
|
||||
public reservedQuoteAsBaseHighestBid: I80F48,
|
||||
public baseInfoIndex: number,
|
||||
public quoteInfoIndex: number,
|
||||
public marketIndex: MarketIndex,
|
||||
|
@ -1540,6 +1565,8 @@ export class Serum3Info {
|
|||
return new Serum3Info(
|
||||
I80F48.from(dto.reservedBase),
|
||||
I80F48.from(dto.reservedQuote),
|
||||
I80F48.from(dto.reservedBaseAsQuoteLowestAsk),
|
||||
I80F48.from(dto.reservedQuoteAsBaseHighestBid),
|
||||
dto.baseInfoIndex,
|
||||
dto.quoteInfoIndex,
|
||||
dto.marketIndex as MarketIndex,
|
||||
|
@ -1552,6 +1579,8 @@ export class Serum3Info {
|
|||
quoteEntryIndex: number,
|
||||
): Serum3Info {
|
||||
return new Serum3Info(
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
baseEntryIndex,
|
||||
|
@ -1561,6 +1590,7 @@ export class Serum3Info {
|
|||
}
|
||||
|
||||
static fromOoModifyingTokenInfos(
|
||||
serumAccount: Serum3Orders,
|
||||
baseInfoIndex: number,
|
||||
baseInfo: TokenInfo,
|
||||
quoteInfoIndex: number,
|
||||
|
@ -1582,9 +1612,18 @@ export class Serum3Info {
|
|||
oo.quoteTokenTotal.sub(oo.quoteTokenFree),
|
||||
);
|
||||
|
||||
const reservedBaseAsQuoteLowestAsk = reservedBase.mul(
|
||||
I80F48.fromNumber(serumAccount.lowestPlacedAsk),
|
||||
);
|
||||
const reservedQuoteAsBaseHighestBid = reservedQuote.mul(
|
||||
I80F48.fromNumber(serumAccount.highestPlacedBidInv),
|
||||
);
|
||||
|
||||
return new Serum3Info(
|
||||
reservedBase,
|
||||
reservedQuote,
|
||||
reservedBaseAsQuoteLowestAsk,
|
||||
reservedQuoteAsBaseHighestBid,
|
||||
baseInfoIndex,
|
||||
quoteInfoIndex,
|
||||
marketIndex,
|
||||
|
@ -1973,6 +2012,8 @@ export class TokenInfoDto {
|
|||
export class Serum3InfoDto {
|
||||
reservedBase: I80F48Dto;
|
||||
reservedQuote: I80F48Dto;
|
||||
reservedBaseAsQuoteLowestAsk: I80F48Dto;
|
||||
reservedQuoteAsBaseHighestBid: I80F48Dto;
|
||||
baseInfoIndex: number;
|
||||
quoteInfoIndex: number;
|
||||
marketIndex: number;
|
||||
|
|
|
@ -182,6 +182,17 @@ export class MangoAccount {
|
|||
return this.frozenUntil.lt(new BN(Date.now() / 1000));
|
||||
}
|
||||
|
||||
public async tokenPositionsForNotConfidentOrStaleOracles(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
): Promise<Bank[]> {
|
||||
const nowSlot = await client.connection.getSlot();
|
||||
|
||||
return this.tokensActive()
|
||||
.map((tp) => group.getFirstBankByTokenIndex(tp.tokenIndex))
|
||||
.filter((bank) => bank.isOracleStaleOrUnconfident(nowSlot));
|
||||
}
|
||||
|
||||
public tokensActive(): TokenPosition[] {
|
||||
return this.tokens.filter((token) => token.isActive());
|
||||
}
|
||||
|
@ -1250,6 +1261,8 @@ export class Serum3Orders {
|
|||
dto.marketIndex as MarketIndex,
|
||||
dto.baseTokenIndex as TokenIndex,
|
||||
dto.quoteTokenIndex as TokenIndex,
|
||||
dto.highestPlacedBidInv,
|
||||
dto.lowestPlacedAsk,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1258,6 +1271,8 @@ export class Serum3Orders {
|
|||
public marketIndex: MarketIndex,
|
||||
public baseTokenIndex: TokenIndex,
|
||||
public quoteTokenIndex: TokenIndex,
|
||||
public highestPlacedBidInv: number,
|
||||
public lowestPlacedAsk: number,
|
||||
) {}
|
||||
|
||||
public isActive(): boolean {
|
||||
|
@ -1273,6 +1288,8 @@ export class Serum3PositionDto {
|
|||
public quoteBorrowsWithoutFee: BN,
|
||||
public baseTokenIndex: number,
|
||||
public quoteTokenIndex: number,
|
||||
public highestPlacedBidInv: number,
|
||||
public lowestPlacedAsk: number,
|
||||
public reserved: number[],
|
||||
) {}
|
||||
}
|
||||
|
@ -1807,15 +1824,24 @@ export class PerpOoDto {
|
|||
) {}
|
||||
}
|
||||
|
||||
export class TokenConditionalSwapDisplayPriceStyle {
|
||||
static sellTokenPerBuyToken = { sellTokenPerBuyToken: {} };
|
||||
static buyTokenPerSellToken = { buyTokenPerSellToken: {} };
|
||||
export type TokenConditionalSwapDisplayPriceStyle =
|
||||
| { sellTokenPerBuyToken: Record<string, never> }
|
||||
| { buyTokenPerSellToken: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace TokenConditionalSwapDisplayPriceStyle {
|
||||
export const sellTokenPerBuyToken = { sellTokenPerBuyToken: {} };
|
||||
export const buyTokenPerSellToken = { buyTokenPerSellToken: {} };
|
||||
}
|
||||
|
||||
export class TokenConditionalSwapIntention {
|
||||
static unknown = { unknown: {} };
|
||||
static stopLoss = { stopLoss: {} };
|
||||
static takeProfit = { takeProfit: {} };
|
||||
export type TokenConditionalSwapIntention =
|
||||
| { unknown: Record<string, never> }
|
||||
| { stopLoss: Record<string, never> }
|
||||
| { takeProfit: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace TokenConditionalSwapIntention {
|
||||
export const unknown = { unknown: {} };
|
||||
export const stopLoss = { stopLoss: {} };
|
||||
export const takeProfit = { takeProfit: {} };
|
||||
}
|
||||
|
||||
function tokenConditionalSwapIntentionFromDto(
|
||||
|
@ -1854,7 +1880,7 @@ export class TokenConditionalSwap {
|
|||
dto.hasData == 1,
|
||||
dto.allowCreatingDeposits == 1,
|
||||
dto.allowCreatingBorrows == 1,
|
||||
dto.priceDisplayStyle == 0
|
||||
dto.displayPriceStyle == 0
|
||||
? TokenConditionalSwapDisplayPriceStyle.sellTokenPerBuyToken
|
||||
: TokenConditionalSwapDisplayPriceStyle.buyTokenPerSellToken,
|
||||
tokenConditionalSwapIntentionFromDto(dto.intention),
|
||||
|
@ -1906,26 +1932,6 @@ export class TokenConditionalSwap {
|
|||
return this.expiryTimestamp.toNumber();
|
||||
}
|
||||
|
||||
// TODO: will be replaced by onchain enum in next release
|
||||
private getTokenConditionalSwapDisplayPriceStyle(group: Group): boolean {
|
||||
const buyBank = this.getBuyToken(group);
|
||||
const sellBank = this.getSellToken(group);
|
||||
|
||||
// If we are tp/sl'ing SOL borrow, then price is stored in sol/usdc
|
||||
// then don't flip
|
||||
if (sellBank.tokenIndex == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// E.g.
|
||||
// If we are tp/sl'ing SOL deposit, then price is stored in usdc/sol
|
||||
if (this.maxSell.eq(U64_MAX_BN)) {
|
||||
true; // dont flip, i.e. continue using sellTokenPerBuyTokenUi price
|
||||
}
|
||||
// Flip the price if we know we are selling an exact amount of SOL
|
||||
return false; // flip, i.e. use buyTokenPerSellTokenUi price
|
||||
}
|
||||
|
||||
private priceLimitToUi(
|
||||
group: Group,
|
||||
sellTokenPerBuyTokenNative: number,
|
||||
|
@ -1943,7 +1949,10 @@ export class TokenConditionalSwap {
|
|||
// buytoken/selltoken or selltoken/buytoken
|
||||
|
||||
// Buy limit / close short
|
||||
if (this.getTokenConditionalSwapDisplayPriceStyle(group)) {
|
||||
if (
|
||||
this.priceDisplayStyle ==
|
||||
TokenConditionalSwapDisplayPriceStyle.sellTokenPerBuyToken
|
||||
) {
|
||||
return roundTo5(sellTokenPerBuyTokenUi);
|
||||
}
|
||||
|
||||
|
@ -1990,7 +1999,10 @@ export class TokenConditionalSwap {
|
|||
// buytoken/selltoken or selltoken/buytoken
|
||||
|
||||
// Buy limit / close short
|
||||
if (this.getTokenConditionalSwapDisplayPriceStyle(group)) {
|
||||
if (
|
||||
this.priceDisplayStyle ==
|
||||
TokenConditionalSwapDisplayPriceStyle.sellTokenPerBuyToken
|
||||
) {
|
||||
return roundTo5(sellTokenPerBuyTokenUi);
|
||||
}
|
||||
|
||||
|
@ -2004,6 +2016,62 @@ export class TokenConditionalSwap {
|
|||
return this.pricePremiumRate * 100;
|
||||
}
|
||||
|
||||
getCurrentlySuggestedPremium(group: Group): number {
|
||||
const buyBank = this.getBuyToken(group);
|
||||
const sellBank = this.getSellToken(group);
|
||||
return TokenConditionalSwap.computePremium(
|
||||
group,
|
||||
buyBank,
|
||||
sellBank,
|
||||
this.maxBuy,
|
||||
this.maxSell,
|
||||
this.getMaxBuyUi(group),
|
||||
this.getMaxSellUi(group),
|
||||
);
|
||||
}
|
||||
|
||||
static computePremium(
|
||||
group: Group,
|
||||
buyBank: Bank,
|
||||
sellBank: Bank,
|
||||
maxBuy: BN,
|
||||
maxSell: BN,
|
||||
maxBuyUi: number,
|
||||
maxSellUi: number,
|
||||
): number {
|
||||
const buyAmountInUsd =
|
||||
maxBuy != U64_MAX_BN
|
||||
? maxBuyUi * buyBank.uiPrice
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
const sellAmountInUsd =
|
||||
maxSell != U64_MAX_BN
|
||||
? maxSellUi * sellBank.uiPrice
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// Used for computing optimal premium
|
||||
let liqorTcsChunkSizeInUsd = Math.min(buyAmountInUsd, sellAmountInUsd);
|
||||
if (liqorTcsChunkSizeInUsd > 5000) {
|
||||
liqorTcsChunkSizeInUsd = 5000;
|
||||
}
|
||||
// For small TCS swaps, reduce chunk size to 1000 USD
|
||||
else {
|
||||
liqorTcsChunkSizeInUsd = 1000;
|
||||
}
|
||||
|
||||
const buyTokenPriceImpact = group.getPriceImpactByTokenIndex(
|
||||
buyBank.tokenIndex,
|
||||
liqorTcsChunkSizeInUsd,
|
||||
);
|
||||
const sellTokenPriceImpact = group.getPriceImpactByTokenIndex(
|
||||
sellBank.tokenIndex,
|
||||
liqorTcsChunkSizeInUsd,
|
||||
);
|
||||
return (
|
||||
((1 + buyTokenPriceImpact / 100) * (1 + sellTokenPriceImpact / 100) - 1) *
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
getBuyToken(group: Group): Bank {
|
||||
return group.getFirstBankByTokenIndex(this.buyTokenIndex);
|
||||
}
|
||||
|
@ -2059,7 +2127,7 @@ export class TokenConditionalSwapDto {
|
|||
public hasData: number,
|
||||
public allowCreatingDeposits: number,
|
||||
public allowCreatingBorrows: number,
|
||||
public priceDisplayStyle: number,
|
||||
public displayPriceStyle: number,
|
||||
public intention: number,
|
||||
) {}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Magic as PythMagic } from '@pythnetwork/client';
|
||||
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
|
||||
import SwitchboardProgram from '@switchboard-xyz/sbv2-lite';
|
||||
import Big from 'big.js';
|
||||
import BN from 'bn.js';
|
||||
import { I80F48, I80F48Dto } from '../numbers/I80F48';
|
||||
|
||||
|
@ -63,26 +64,47 @@ export class StubOracle {
|
|||
export function parseSwitchboardOracleV1(accountInfo: AccountInfo<Buffer>): {
|
||||
price: number;
|
||||
lastUpdatedSlot: number;
|
||||
uiDeviation: number;
|
||||
} {
|
||||
const price = accountInfo.data.readDoubleLE(1 + 32 + 4 + 4);
|
||||
const lastUpdatedSlot = parseInt(
|
||||
accountInfo.data.readBigUInt64LE(1 + 32 + 4 + 4 + 8).toString(),
|
||||
);
|
||||
return { price, lastUpdatedSlot };
|
||||
const minResponse = accountInfo.data.readDoubleLE(1 + 32 + 4 + 4 + 8 + 8 + 8);
|
||||
const maxResponse = accountInfo.data.readDoubleLE(
|
||||
1 + 32 + 4 + 4 + 8 + 8 + 8 + 8,
|
||||
);
|
||||
return { price, lastUpdatedSlot, uiDeviation: maxResponse - minResponse };
|
||||
}
|
||||
|
||||
export function switchboardDecimalToBig(sbDecimal: {
|
||||
mantissa: BN;
|
||||
scale: number;
|
||||
}): Big {
|
||||
const mantissa = new Big(sbDecimal.mantissa.toString());
|
||||
const scale = sbDecimal.scale;
|
||||
const oldDp = Big.DP;
|
||||
Big.DP = 20;
|
||||
const result: Big = mantissa.div(new Big(10).pow(scale));
|
||||
Big.DP = oldDp;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseSwitchboardOracleV2(
|
||||
program: SwitchboardProgram,
|
||||
accountInfo: AccountInfo<Buffer>,
|
||||
): { price: number; lastUpdatedSlot: number } {
|
||||
): { price: number; lastUpdatedSlot: number; uiDeviation: number } {
|
||||
const price = program.decodeLatestAggregatorValue(accountInfo)!.toNumber();
|
||||
const lastUpdatedSlot = program
|
||||
.decodeAggregator(accountInfo)
|
||||
.latestConfirmedRound!.roundOpenSlot!.toNumber();
|
||||
const stdDeviation = switchboardDecimalToBig(
|
||||
program.decodeAggregator(accountInfo).latestConfirmedRound.stdDeviation,
|
||||
);
|
||||
|
||||
if (!price || !lastUpdatedSlot)
|
||||
throw new Error('Unable to parse Switchboard Oracle V2');
|
||||
return { price, lastUpdatedSlot };
|
||||
return { price, lastUpdatedSlot, uiDeviation: stdDeviation.toNumber() };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -93,7 +115,7 @@ export function parseSwitchboardOracleV2(
|
|||
export async function parseSwitchboardOracle(
|
||||
accountInfo: AccountInfo<Buffer>,
|
||||
connection: Connection,
|
||||
): Promise<{ price: number; lastUpdatedSlot: number }> {
|
||||
): Promise<{ price: number; lastUpdatedSlot: number; uiDeviation: number }> {
|
||||
if (accountInfo.owner.equals(SwitchboardProgram.devnetPid)) {
|
||||
if (!sbv2DevnetProgram) {
|
||||
sbv2DevnetProgram = await SwitchboardProgram.loadDevnet(connection);
|
||||
|
@ -133,3 +155,26 @@ export function isSwitchboardOracle(accountInfo: AccountInfo<Buffer>): boolean {
|
|||
export function isPythOracle(accountInfo: AccountInfo<Buffer>): boolean {
|
||||
return accountInfo.data.readUInt32LE(0) === PythMagic;
|
||||
}
|
||||
|
||||
export function isOracleStaleOrUnconfident(
|
||||
nowSlot: number,
|
||||
maxStalenessSlots: number,
|
||||
oracleLastUpdatedSlot: number | undefined,
|
||||
deviation: I80F48 | undefined,
|
||||
confFilter: I80F48,
|
||||
price: I80F48,
|
||||
): boolean {
|
||||
if (
|
||||
maxStalenessSlots >= 0 &&
|
||||
oracleLastUpdatedSlot &&
|
||||
nowSlot > oracleLastUpdatedSlot + maxStalenessSlots
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (deviation && deviation.gt(confFilter.mul(price))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from './bank';
|
||||
import { Group } from './group';
|
||||
import { MangoAccount } from './mangoAccount';
|
||||
import { OracleProvider } from './oracle';
|
||||
import { OracleProvider, isOracleStaleOrUnconfident } from './oracle';
|
||||
|
||||
export type PerpMarketIndex = number & As<'perp-market-index'>;
|
||||
|
||||
|
@ -56,6 +56,7 @@ export class PerpMarket {
|
|||
public _price: I80F48;
|
||||
public _uiPrice: number;
|
||||
public _oracleLastUpdatedSlot: number;
|
||||
public _oracleLastKnownDeviation: I80F48 | undefined;
|
||||
public _oracleProvider: OracleProvider;
|
||||
|
||||
public _bids: BookSide;
|
||||
|
@ -244,6 +245,17 @@ export class PerpMarket {
|
|||
.toNumber();
|
||||
}
|
||||
|
||||
isOracleStaleOrUnconfident(nowSlot: number): boolean {
|
||||
return isOracleStaleOrUnconfident(
|
||||
nowSlot,
|
||||
this.oracleConfig.maxStalenessSlots.toNumber(),
|
||||
this.oracleLastUpdatedSlot,
|
||||
this._oracleLastKnownDeviation,
|
||||
this.oracleConfig.confFilter,
|
||||
this.price,
|
||||
);
|
||||
}
|
||||
|
||||
get price(): I80F48 {
|
||||
if (this._price === undefined) {
|
||||
throw new Error(
|
||||
|
@ -646,11 +658,7 @@ export class BookSide {
|
|||
* iterates over all orders
|
||||
*/
|
||||
public *items(): Generator<PerpOrder> {
|
||||
function isBetter(
|
||||
type: PerpOrderSide,
|
||||
a: PerpOrder,
|
||||
b: PerpOrder,
|
||||
): boolean {
|
||||
function isBetter(type: BookSideType, a: PerpOrder, b: PerpOrder): boolean {
|
||||
return a.priceLots.eq(b.priceLots)
|
||||
? a.seqNum.lt(b.seqNum) // if prices are equal prefer perp orders in the order they are placed
|
||||
: type === BookSideType.bids // else compare the actual prices
|
||||
|
@ -833,10 +841,15 @@ export class BookSide {
|
|||
}
|
||||
}
|
||||
|
||||
export class BookSideType {
|
||||
static bids = { bids: {} };
|
||||
static asks = { asks: {} };
|
||||
export type BookSideType =
|
||||
| { bids: Record<string, never> }
|
||||
| { asks: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace BookSideType {
|
||||
export const bids = { bids: {} };
|
||||
export const asks = { asks: {} };
|
||||
}
|
||||
|
||||
export class LeafNode {
|
||||
static from(obj: {
|
||||
ownerSlot: number;
|
||||
|
@ -879,23 +892,39 @@ export class InnerNode {
|
|||
constructor(public children: [number]) {}
|
||||
}
|
||||
|
||||
export class PerpSelfTradeBehavior {
|
||||
static decrementTake = { decrementTake: {} };
|
||||
static cancelProvide = { cancelProvide: {} };
|
||||
static abortTransaction = { abortTransaction: {} };
|
||||
export type PerpSelfTradeBehavior =
|
||||
| { decrementTake: Record<string, never> }
|
||||
| { cancelProvide: Record<string, never> }
|
||||
| { abortTransaction: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace PerpSelfTradeBehavior {
|
||||
export const decrementTake = { decrementTake: {} };
|
||||
export const cancelProvide = { cancelProvide: {} };
|
||||
export const abortTransaction = { abortTransaction: {} };
|
||||
}
|
||||
|
||||
export class PerpOrderSide {
|
||||
static bid = { bid: {} };
|
||||
static ask = { ask: {} };
|
||||
export type PerpOrderSide =
|
||||
| { bid: Record<string, never> }
|
||||
| { ask: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace PerpOrderSide {
|
||||
export const bid = { bid: {} };
|
||||
export const ask = { ask: {} };
|
||||
}
|
||||
|
||||
export class PerpOrderType {
|
||||
static limit = { limit: {} };
|
||||
static immediateOrCancel = { immediateOrCancel: {} };
|
||||
static postOnly = { postOnly: {} };
|
||||
static market = { market: {} };
|
||||
static postOnlySlide = { postOnlySlide: {} };
|
||||
export type PerpOrderType =
|
||||
| { limit: Record<string, never> }
|
||||
| { immediateOrCancel: Record<string, never> }
|
||||
| { postOnly: Record<string, never> }
|
||||
| { market: Record<string, never> }
|
||||
| { postOnlySlide: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace PerpOrderType {
|
||||
export const limit = { limit: {} };
|
||||
export const immediateOrCancel = { immediateOrCancel: {} };
|
||||
export const postOnly = { postOnly: {} };
|
||||
export const market = { market: {} };
|
||||
export const postOnlySlide = { postOnlySlide: {} };
|
||||
}
|
||||
|
||||
export class PerpOrder {
|
||||
|
|
|
@ -197,21 +197,35 @@ export class Serum3Market {
|
|||
}
|
||||
}
|
||||
|
||||
export class Serum3SelfTradeBehavior {
|
||||
static decrementTake = { decrementTake: {} };
|
||||
static cancelProvide = { cancelProvide: {} };
|
||||
static abortTransaction = { abortTransaction: {} };
|
||||
export type Serum3OrderType =
|
||||
| { limit: Record<string, never> }
|
||||
| { immediateOrCancel: Record<string, never> }
|
||||
| { postOnly: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Serum3OrderType {
|
||||
export const limit = { limit: {} };
|
||||
export const immediateOrCancel = { immediateOrCancel: {} };
|
||||
export const postOnly = { postOnly: {} };
|
||||
}
|
||||
|
||||
export class Serum3OrderType {
|
||||
static limit = { limit: {} };
|
||||
static immediateOrCancel = { immediateOrCancel: {} };
|
||||
static postOnly = { postOnly: {} };
|
||||
export type Serum3SelfTradeBehavior =
|
||||
| { decrementTake: Record<string, never> }
|
||||
| { cancelProvide: Record<string, never> }
|
||||
| { abortTransaction: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Serum3SelfTradeBehavior {
|
||||
export const decrementTake = { decrementTake: {} };
|
||||
export const cancelProvide = { cancelProvide: {} };
|
||||
export const abortTransaction = { abortTransaction: {} };
|
||||
}
|
||||
|
||||
export class Serum3Side {
|
||||
static bid = { bid: {} };
|
||||
static ask = { ask: {} };
|
||||
export type Serum3Side =
|
||||
| { bid: Record<string, never> }
|
||||
| { ask: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Serum3Side {
|
||||
export const bid = { bid: {} };
|
||||
export const ask = { ask: {} };
|
||||
}
|
||||
|
||||
export async function generateSerum3MarketExternalVaultSignerAddress(
|
||||
|
|
|
@ -17,16 +17,16 @@ import {
|
|||
AddressLookupTableAccount,
|
||||
Cluster,
|
||||
Commitment,
|
||||
ComputeBudgetProgram,
|
||||
Connection,
|
||||
Keypair,
|
||||
MemcmpFilter,
|
||||
PublicKey,
|
||||
RecentPrioritizationFees,
|
||||
SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
TransactionSignature,
|
||||
RecentPrioritizationFees,
|
||||
} from '@solana/web3.js';
|
||||
import bs58 from 'bs58';
|
||||
import chunk from 'lodash/chunk';
|
||||
|
@ -41,6 +41,7 @@ import {
|
|||
MangoAccount,
|
||||
PerpPosition,
|
||||
Serum3Orders,
|
||||
TokenConditionalSwap,
|
||||
TokenConditionalSwapDisplayPriceStyle,
|
||||
TokenConditionalSwapDto,
|
||||
TokenConditionalSwapIntention,
|
||||
|
@ -69,8 +70,8 @@ import {
|
|||
IxGateParams,
|
||||
PerpEditParams,
|
||||
TokenEditParams,
|
||||
buildIxGate,
|
||||
TokenRegisterParams,
|
||||
buildIxGate,
|
||||
} from './clientIxParamBuilder';
|
||||
import {
|
||||
MANGO_V4_ID,
|
||||
|
@ -81,7 +82,7 @@ import {
|
|||
import { Id } from './ids';
|
||||
import { IDL, MangoV4 } from './mango_v4';
|
||||
import { I80F48 } from './numbers/I80F48';
|
||||
import { FlashLoanType, InterestRateParams, OracleConfigParams } from './types';
|
||||
import { FlashLoanType, OracleConfigParams } from './types';
|
||||
import {
|
||||
I64_MAX_BN,
|
||||
U64_MAX_BN,
|
||||
|
@ -94,6 +95,9 @@ import { MangoSignatureStatus, sendTransaction } from './utils/rpc';
|
|||
import { NATIVE_MINT, TOKEN_PROGRAM_ID } from './utils/spl';
|
||||
|
||||
export const DEFAULT_TOKEN_CONDITIONAL_SWAP_COUNT = 8;
|
||||
export const PERP_SETTLE_PNL_CU_LIMIT = 250000;
|
||||
export const PERP_SETTLE_FEES_CU_LIMIT = 20000;
|
||||
export const SERUM_SETTLE_FUNDS_CU_LIMIT = 65000;
|
||||
|
||||
export enum AccountRetriever {
|
||||
Scanning,
|
||||
|
@ -1390,6 +1394,65 @@ export class MangoClient {
|
|||
]);
|
||||
}
|
||||
|
||||
public async tokenWithdrawAllDepositForAllUnconfidentOrStaleOracles(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const nowSlot = await this.connection.getSlot();
|
||||
|
||||
const banksToWithdrawFrom = Array.from(group.banksMapByTokenIndex.values())
|
||||
.map((banks) => banks[0])
|
||||
.filter((bank) => bank.isOracleStaleOrUnconfident(nowSlot))
|
||||
.filter((b) => mangoAccount.getTokenBalanceUi(b) > 0);
|
||||
|
||||
if (banksToWithdrawFrom.length === 0) {
|
||||
throw new Error(`No stale oracle or bad confidence oracle deposits!`);
|
||||
}
|
||||
|
||||
const ixs = await Promise.all(
|
||||
banksToWithdrawFrom.map((bank) => {
|
||||
return this.tokenWithdrawNativeIx(
|
||||
group,
|
||||
mangoAccount,
|
||||
bank.mint,
|
||||
U64_MAX_BN,
|
||||
false,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(group, ixs.flat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw the entire deposit balance for a token, effectively freeing the token position
|
||||
*
|
||||
* @param group
|
||||
* @param mangoAccount
|
||||
* @param mintPk
|
||||
* @returns
|
||||
*/
|
||||
public async tokenWithdrawAllDepositForMint(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
mintPk: PublicKey,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const bank = group.getFirstBankByMint(mintPk);
|
||||
const b = mangoAccount.getTokenBalance(bank).toNumber();
|
||||
if (b < 0) {
|
||||
throw new Error(`Only call this method for deposits and not borrows!`);
|
||||
}
|
||||
const ixs = await this.tokenWithdrawNativeIx(
|
||||
group,
|
||||
mangoAccount,
|
||||
mintPk,
|
||||
U64_MAX_BN,
|
||||
false,
|
||||
);
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(group, ixs);
|
||||
}
|
||||
|
||||
public async tokenWithdraw(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
|
@ -1398,7 +1461,7 @@ export class MangoClient {
|
|||
allowBorrow: boolean,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const nativeAmount = toNative(amount, group.getMintDecimals(mintPk));
|
||||
const ixes = await this.tokenWithdrawNativeIx(
|
||||
const ixs = await this.tokenWithdrawNativeIx(
|
||||
group,
|
||||
mangoAccount,
|
||||
mintPk,
|
||||
|
@ -1406,7 +1469,7 @@ export class MangoClient {
|
|||
allowBorrow,
|
||||
);
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(group, ixes);
|
||||
return await this.sendAndConfirmTransactionForGroup(group, ixs);
|
||||
}
|
||||
|
||||
public async tokenWithdrawNativeIx(
|
||||
|
@ -1905,7 +1968,7 @@ export class MangoClient {
|
|||
clientOrderId: number,
|
||||
limit: number,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const placeOrderIxes = await this.serum3PlaceOrderIx(
|
||||
const placeOrderIxs = await this.serum3PlaceOrderIx(
|
||||
group,
|
||||
mangoAccount,
|
||||
externalMarketPk,
|
||||
|
@ -1924,7 +1987,7 @@ export class MangoClient {
|
|||
externalMarketPk,
|
||||
);
|
||||
|
||||
const ixs = [...placeOrderIxes, settleIx];
|
||||
const ixs = [...placeOrderIxs, settleIx];
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(group, ixs);
|
||||
}
|
||||
|
@ -2113,7 +2176,7 @@ export class MangoClient {
|
|||
side: Serum3Side,
|
||||
orderId: BN,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const ixes = await Promise.all([
|
||||
const ixs = await Promise.all([
|
||||
this.serum3CancelOrderIx(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
@ -2124,7 +2187,7 @@ export class MangoClient {
|
|||
this.serum3SettleFundsV2Ix(group, mangoAccount, externalMarketPk),
|
||||
]);
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(group, ixes);
|
||||
return await this.sendAndConfirmTransactionForGroup(group, ixs);
|
||||
}
|
||||
|
||||
/// perps
|
||||
|
@ -2879,7 +2942,7 @@ export class MangoClient {
|
|||
continue;
|
||||
}
|
||||
ixs1.push(
|
||||
// Takes ~130k CU
|
||||
// Takes ~250k CU
|
||||
await this.perpSettlePnlIx(
|
||||
group,
|
||||
pa.getUnsettledPnlUi(pm) > 0 ? mangoAccount : candidates[0].account,
|
||||
|
@ -2912,9 +2975,10 @@ export class MangoClient {
|
|||
);
|
||||
|
||||
if (
|
||||
mangoAccount.perpActive().length * 150 +
|
||||
mangoAccount.serum3Active().length * 65 >
|
||||
1600
|
||||
mangoAccount.perpActive().length *
|
||||
(PERP_SETTLE_PNL_CU_LIMIT + PERP_SETTLE_FEES_CU_LIMIT) +
|
||||
mangoAccount.serum3Active().length * SERUM_SETTLE_FUNDS_CU_LIMIT >
|
||||
1600000
|
||||
) {
|
||||
throw new Error(
|
||||
`Too many perp positions and serum open orders to settle in one tx! Please try settling individually!`,
|
||||
|
@ -2923,7 +2987,16 @@ export class MangoClient {
|
|||
|
||||
return await this.sendAndConfirmTransactionForGroup(
|
||||
group,
|
||||
[...ixs1, ...ixs2],
|
||||
[
|
||||
ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units:
|
||||
mangoAccount.perpActive().length *
|
||||
(PERP_SETTLE_PNL_CU_LIMIT + PERP_SETTLE_FEES_CU_LIMIT) +
|
||||
mangoAccount.serum3Active().length * SERUM_SETTLE_FUNDS_CU_LIMIT,
|
||||
}),
|
||||
...ixs1,
|
||||
...ixs2,
|
||||
],
|
||||
{
|
||||
prioritizationFee: true,
|
||||
},
|
||||
|
@ -2940,6 +3013,9 @@ export class MangoClient {
|
|||
maxSettleAmount?: number,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
return await this.sendAndConfirmTransactionForGroup(group, [
|
||||
ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: PERP_SETTLE_PNL_CU_LIMIT + PERP_SETTLE_FEES_CU_LIMIT,
|
||||
}),
|
||||
await this.perpSettlePnlIx(
|
||||
group,
|
||||
profitableAccount,
|
||||
|
@ -2964,6 +3040,9 @@ export class MangoClient {
|
|||
perpMarketIndex: PerpMarketIndex,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
return await this.sendAndConfirmTransactionForGroup(group, [
|
||||
ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: PERP_SETTLE_PNL_CU_LIMIT,
|
||||
}),
|
||||
await this.perpSettlePnlIx(
|
||||
group,
|
||||
profitableAccount,
|
||||
|
@ -3416,9 +3495,9 @@ export class MangoClient {
|
|||
account: MangoAccount,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
thresholdPriceUi: number,
|
||||
thresholdPrice: number,
|
||||
thresholdPriceInSellPerBuyToken: boolean,
|
||||
maxSellUi: number | null,
|
||||
maxSell: number | null,
|
||||
pricePremium: number | null,
|
||||
expiryTimestamp: number | null,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
|
@ -3431,15 +3510,15 @@ export class MangoClient {
|
|||
}
|
||||
|
||||
if (!thresholdPriceInSellPerBuyToken) {
|
||||
thresholdPriceUi = 1 / thresholdPriceUi;
|
||||
thresholdPrice = 1 / thresholdPrice;
|
||||
}
|
||||
const thresholdPrice = toNativeSellPerBuyTokenPrice(
|
||||
thresholdPriceUi,
|
||||
const thresholdPriceNativeNative = toNativeSellPerBuyTokenPrice(
|
||||
thresholdPrice,
|
||||
sellBank,
|
||||
buyBank,
|
||||
);
|
||||
const lowerLimit = 0;
|
||||
const upperLimit = thresholdPrice;
|
||||
const upperLimit = thresholdPriceNativeNative;
|
||||
|
||||
return await this.tokenConditionalSwapCreate(
|
||||
group,
|
||||
|
@ -3449,7 +3528,7 @@ export class MangoClient {
|
|||
lowerLimit,
|
||||
upperLimit,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
maxSellUi ?? account.getTokenBalanceUi(sellBank),
|
||||
maxSell ?? account.getTokenBalanceUi(sellBank),
|
||||
'TakeProfitOnDeposit',
|
||||
pricePremium,
|
||||
true,
|
||||
|
@ -3464,9 +3543,9 @@ export class MangoClient {
|
|||
account: MangoAccount,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
thresholdPriceUi: number,
|
||||
thresholdPrice: number,
|
||||
thresholdPriceInSellPerBuyToken: boolean,
|
||||
maxSellUi: number | null,
|
||||
maxSell: number | null,
|
||||
pricePremium: number | null,
|
||||
expiryTimestamp: number | null,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
|
@ -3479,14 +3558,14 @@ export class MangoClient {
|
|||
}
|
||||
|
||||
if (!thresholdPriceInSellPerBuyToken) {
|
||||
thresholdPriceUi = 1 / thresholdPriceUi;
|
||||
thresholdPrice = 1 / thresholdPrice;
|
||||
}
|
||||
const thresholdPrice = toNativeSellPerBuyTokenPrice(
|
||||
thresholdPriceUi,
|
||||
const thresholdPriceNativeNative = toNativeSellPerBuyTokenPrice(
|
||||
thresholdPrice,
|
||||
sellBank,
|
||||
buyBank,
|
||||
);
|
||||
const lowerLimit = thresholdPrice;
|
||||
const lowerLimit = thresholdPriceNativeNative;
|
||||
const upperLimit = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
return await this.tokenConditionalSwapCreate(
|
||||
|
@ -3497,7 +3576,7 @@ export class MangoClient {
|
|||
lowerLimit,
|
||||
upperLimit,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
maxSellUi ?? account.getTokenBalanceUi(sellBank),
|
||||
maxSell ?? account.getTokenBalanceUi(sellBank),
|
||||
'StopLossOnDeposit',
|
||||
pricePremium,
|
||||
true,
|
||||
|
@ -3512,7 +3591,7 @@ export class MangoClient {
|
|||
account: MangoAccount,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
thresholdPriceUi: number,
|
||||
thresholdPrice: number,
|
||||
thresholdPriceInSellPerBuyToken: boolean,
|
||||
maxBuyUi: number | null,
|
||||
pricePremium: number | null,
|
||||
|
@ -3528,15 +3607,15 @@ export class MangoClient {
|
|||
}
|
||||
|
||||
if (!thresholdPriceInSellPerBuyToken) {
|
||||
thresholdPriceUi = 1 / thresholdPriceUi;
|
||||
thresholdPrice = 1 / thresholdPrice;
|
||||
}
|
||||
const thresholdPrice = toNativeSellPerBuyTokenPrice(
|
||||
thresholdPriceUi,
|
||||
const thresholdPriceNativeNative = toNativeSellPerBuyTokenPrice(
|
||||
thresholdPrice,
|
||||
sellBank,
|
||||
buyBank,
|
||||
);
|
||||
const lowerLimit = thresholdPrice;
|
||||
const upperLimit = Number.MAX_SAFE_INTEGER;
|
||||
const lowerLimit = 0;
|
||||
const upperLimit = thresholdPriceNativeNative;
|
||||
|
||||
return await this.tokenConditionalSwapCreate(
|
||||
group,
|
||||
|
@ -3561,7 +3640,7 @@ export class MangoClient {
|
|||
account: MangoAccount,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
thresholdPriceUi: number,
|
||||
thresholdPrice: number,
|
||||
thresholdPriceInSellPerBuyToken: boolean,
|
||||
maxBuyUi: number | null,
|
||||
pricePremium: number | null,
|
||||
|
@ -3577,15 +3656,15 @@ export class MangoClient {
|
|||
}
|
||||
|
||||
if (!thresholdPriceInSellPerBuyToken) {
|
||||
thresholdPriceUi = 1 / thresholdPriceUi;
|
||||
thresholdPrice = 1 / thresholdPrice;
|
||||
}
|
||||
const thresholdPrice = toNativeSellPerBuyTokenPrice(
|
||||
thresholdPriceUi,
|
||||
const thresholdPriceNativeNative = toNativeSellPerBuyTokenPrice(
|
||||
thresholdPrice,
|
||||
sellBank,
|
||||
buyBank,
|
||||
);
|
||||
const lowerLimit = 0;
|
||||
const upperLimit = thresholdPrice;
|
||||
const lowerLimit = thresholdPriceNativeNative;
|
||||
const upperLimit = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
return await this.tokenConditionalSwapCreate(
|
||||
group,
|
||||
|
@ -3610,10 +3689,10 @@ export class MangoClient {
|
|||
account: MangoAccount,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
lowerLimit: number,
|
||||
upperLimit: number,
|
||||
maxBuyUi: number,
|
||||
maxSellUi: number,
|
||||
lowerLimitNativeNative: number,
|
||||
upperLimitNativeNative: number,
|
||||
maxBuy: number,
|
||||
maxSell: number,
|
||||
tcsIntention:
|
||||
| 'TakeProfitOnDeposit'
|
||||
| 'StopLossOnDeposit'
|
||||
|
@ -3626,47 +3705,23 @@ export class MangoClient {
|
|||
expiryTimestamp: number | null,
|
||||
displayPriceInSellTokenPerBuyToken: boolean,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
let maxBuy, maxSell, buyAmountInUsd, sellAmountInUsd;
|
||||
if (maxBuyUi == Number.MAX_SAFE_INTEGER) {
|
||||
maxBuy = U64_MAX_BN;
|
||||
} else {
|
||||
buyAmountInUsd = maxBuyUi * buyBank.uiPrice;
|
||||
maxBuy = toNative(maxBuyUi, buyBank.mintDecimals);
|
||||
}
|
||||
if (maxSellUi == Number.MAX_SAFE_INTEGER) {
|
||||
maxSell = U64_MAX_BN;
|
||||
} else {
|
||||
sellAmountInUsd = maxSellUi * sellBank.uiPrice;
|
||||
maxSell = toNative(maxSellUi, sellBank.mintDecimals);
|
||||
}
|
||||
|
||||
// Used for computing optimal premium
|
||||
let liqorTcsChunkSizeInUsd = Math.min(buyAmountInUsd, sellAmountInUsd);
|
||||
if (liqorTcsChunkSizeInUsd > 5000) {
|
||||
liqorTcsChunkSizeInUsd = 5000;
|
||||
}
|
||||
// For small TCS swaps, reduce chunk size to 1000 USD
|
||||
else {
|
||||
liqorTcsChunkSizeInUsd = 1000;
|
||||
}
|
||||
|
||||
if (!pricePremium) {
|
||||
if (maxBuy.eq(U64_MAX_BN)) {
|
||||
maxSell.toNumber() * sellBank.uiPrice;
|
||||
}
|
||||
const buyTokenPriceImpact = group.getPriceImpactByTokenIndex(
|
||||
buyBank.tokenIndex,
|
||||
liqorTcsChunkSizeInUsd,
|
||||
);
|
||||
const sellTokenPriceImpact = group.getPriceImpactByTokenIndex(
|
||||
sellBank.tokenIndex,
|
||||
liqorTcsChunkSizeInUsd,
|
||||
);
|
||||
pricePremium =
|
||||
((1 + buyTokenPriceImpact / 100) * (1 + sellTokenPriceImpact / 100) -
|
||||
1) *
|
||||
100;
|
||||
}
|
||||
const maxBuyNative =
|
||||
maxBuy == Number.MAX_SAFE_INTEGER
|
||||
? U64_MAX_BN
|
||||
: toNative(maxBuy, buyBank.mintDecimals);
|
||||
const maxSellNative =
|
||||
maxSell == Number.MAX_SAFE_INTEGER
|
||||
? U64_MAX_BN
|
||||
: toNative(maxSell, sellBank.mintDecimals);
|
||||
pricePremium = TokenConditionalSwap.computePremium(
|
||||
group,
|
||||
buyBank,
|
||||
sellBank,
|
||||
maxBuyNative,
|
||||
maxSellNative,
|
||||
maxBuy,
|
||||
maxSell,
|
||||
);
|
||||
const pricePremiumRate = pricePremium > 0 ? pricePremium / 100 : 0.03;
|
||||
|
||||
let intention: TokenConditionalSwapIntention;
|
||||
|
@ -3689,11 +3744,11 @@ export class MangoClient {
|
|||
account,
|
||||
buyBank.mint,
|
||||
sellBank.mint,
|
||||
maxBuy,
|
||||
maxSell,
|
||||
maxBuyNative,
|
||||
maxSellNative,
|
||||
expiryTimestamp,
|
||||
lowerLimit,
|
||||
upperLimit,
|
||||
lowerLimitNativeNative,
|
||||
upperLimitNativeNative,
|
||||
pricePremiumRate,
|
||||
allowCreatingDeposits,
|
||||
allowCreatingBorrows,
|
||||
|
@ -4303,6 +4358,10 @@ export class MangoClient {
|
|||
);
|
||||
if (inactiveTokenPosition != -1) {
|
||||
tokenPositionIndices[inactiveTokenPosition] = bank.tokenIndex;
|
||||
} else {
|
||||
throw new Error(
|
||||
`All token positions are occupied, either expand mango account, or close an existing token position, e.g. by withdrawing token deposits, or repaying all borrows!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@ export const RUST_I64_MIN = (): BN => {
|
|||
return new BN('-9223372036854775807');
|
||||
};
|
||||
|
||||
export const COMPUTE_BUDGET_PROGRAM_ID = new PublicKey(
|
||||
'ComputeBudget111111111111111111111111111111',
|
||||
);
|
||||
|
||||
export const OPENBOOK_PROGRAM_ID = {
|
||||
devnet: new PublicKey('EoTcMgcDRTJVZDMZWBoU6rhYHZfkNTVEAfz3uUJRcYGj'),
|
||||
'mainnet-beta': new PublicKey('srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX'),
|
||||
|
@ -22,7 +26,17 @@ export const MANGO_V4_ID = {
|
|||
'mainnet-beta': new PublicKey('4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg'),
|
||||
};
|
||||
|
||||
export const MANGO_V4_MAIN_GROUP = new PublicKey(
|
||||
'78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX',
|
||||
);
|
||||
|
||||
export const USDC_MINT = new PublicKey(
|
||||
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
);
|
||||
export const MAX_RECENT_PRIORITY_FEE_ACCOUNTS = 128;
|
||||
|
||||
export const JUPITER = {
|
||||
V3: new PublicKey('JUP3c2Uh3WA4Ng34tw6kPd2G4C5BB21Xo36Je1s32Ph'),
|
||||
V4: new PublicKey('JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB'),
|
||||
V6: new PublicKey('JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4'),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { Connection } from '@solana/web3.js';
|
||||
import { JUPITER } from './constants';
|
||||
|
||||
export enum TransactionErrors {
|
||||
// Slippage incurred was higher than user expected
|
||||
JupiterSlippageToleranceExceeded,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
export function containsJupiterProgram(logMessages: string[]): boolean {
|
||||
return (
|
||||
logMessages.some((msg) => msg.includes(JUPITER.V3.toBase58())) ||
|
||||
logMessages.some((msg) => msg.includes(JUPITER.V4.toBase58())) ||
|
||||
logMessages.some((msg) => msg.includes(JUPITER.V6.toBase58()))
|
||||
);
|
||||
}
|
||||
|
||||
export async function parseTxForKnownErrors(
|
||||
connection: Connection,
|
||||
signature: string,
|
||||
): Promise<TransactionErrors> {
|
||||
const tx = await connection.getTransaction(signature, {
|
||||
commitment: 'confirmed',
|
||||
maxSupportedTransactionVersion: 0,
|
||||
});
|
||||
|
||||
if (tx && tx.meta && tx.meta.logMessages) {
|
||||
if (
|
||||
tx.meta.logMessages.some((msg) =>
|
||||
msg.includes('SlippageToleranceExceeded'),
|
||||
) &&
|
||||
containsJupiterProgram(tx.meta.logMessages)
|
||||
) {
|
||||
return TransactionErrors.JupiterSlippageToleranceExceeded;
|
||||
}
|
||||
}
|
||||
|
||||
return TransactionErrors.Unknown;
|
||||
}
|
|
@ -19,6 +19,7 @@ export {
|
|||
buildIxGate,
|
||||
} from './clientIxParamBuilder';
|
||||
export * from './constants';
|
||||
export * from './error';
|
||||
export * from './mango_v4';
|
||||
export * from './numbers/I80F48';
|
||||
export * from './risk';
|
||||
|
|
|
@ -50,6 +50,8 @@ export type PriceImpact = {
|
|||
avg_price_impact_percent: number;
|
||||
min_price_impact_percent: number;
|
||||
max_price_impact_percent: number;
|
||||
p90: number;
|
||||
p95: number;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -62,7 +64,9 @@ export function computePriceImpactOnJup(
|
|||
tokenName: string,
|
||||
): number {
|
||||
try {
|
||||
const closestTo = [1000, 5000, 20000, 100000].reduce((prev, curr) =>
|
||||
const closestTo = [
|
||||
1_000, 5_000, 20_000, 100_000, 250_000, 500_000, 1_000_000, 5_000_000,
|
||||
].reduce((prev, curr) =>
|
||||
Math.abs(curr - usdcAmount) < Math.abs(prev - usdcAmount) ? curr : prev,
|
||||
);
|
||||
// Workaround api
|
||||
|
@ -73,7 +77,7 @@ export function computePriceImpactOnJup(
|
|||
(pi) => pi.symbol == tokenName && pi.target_amount == closestTo,
|
||||
);
|
||||
if (filteredPis.length > 0) {
|
||||
return (filteredPis[0].avg_price_impact_percent * 10000) / 100;
|
||||
return (filteredPis[0].p90 * 10000) / 100;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
|
|
@ -7,9 +7,13 @@ export class FlashLoanWithdraw {
|
|||
static amount: BN;
|
||||
}
|
||||
|
||||
export class FlashLoanType {
|
||||
static unknown = { unknown: {} };
|
||||
static swap = { swap: {} };
|
||||
export type FlashLoanType =
|
||||
| { unknown: Record<string, never> }
|
||||
| { swap: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace FlashLoanType {
|
||||
export const unknown = { unknown: {} };
|
||||
export const swap = { swap: {} };
|
||||
}
|
||||
|
||||
export class InterestRateParams {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { AnchorProvider } from '@coral-xyz/anchor';
|
||||
import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet';
|
||||
import { u8 } from '@solana/buffer-layout';
|
||||
import {
|
||||
AddressLookupTableAccount,
|
||||
ComputeBudgetProgram,
|
||||
|
@ -11,6 +12,7 @@ import {
|
|||
TransactionSignature,
|
||||
VersionedTransaction,
|
||||
} from '@solana/web3.js';
|
||||
import { COMPUTE_BUDGET_PROGRAM_ID } from '../constants';
|
||||
|
||||
export interface MangoSignatureStatus {
|
||||
slot: number;
|
||||
|
@ -37,6 +39,32 @@ export async function sendTransaction(
|
|||
|
||||
const payer = (provider as AnchorProvider).wallet;
|
||||
|
||||
//
|
||||
// setComputeUnitLimit, hard code to a higher minimum, this is needed so that we dont fail simple UI interactions
|
||||
//
|
||||
// https://github.com/solana-labs/solana-web3.js/blob/master/packages/library-legacy/src/programs/compute-budget.ts#L202
|
||||
const computeUnitLimitIxFound = ixs.some(
|
||||
(ix) =>
|
||||
ix.programId.equals(COMPUTE_BUDGET_PROGRAM_ID) &&
|
||||
u8().decode(ix.data.subarray(0, 1)) == 2,
|
||||
);
|
||||
|
||||
if (!computeUnitLimitIxFound) {
|
||||
const totalUserIntendedIxs = ixs.filter(
|
||||
(ix) => !ix.programId.equals(COMPUTE_BUDGET_PROGRAM_ID),
|
||||
).length;
|
||||
const requestCu = Math.min(totalUserIntendedIxs * 250_000, 1_600_000);
|
||||
ixs = [
|
||||
ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: requestCu,
|
||||
}),
|
||||
...ixs,
|
||||
];
|
||||
}
|
||||
|
||||
//
|
||||
// setComputeUnitPrice
|
||||
//
|
||||
if (opts.prioritizationFee) {
|
||||
ixs = [createComputeBudgetIx(opts.prioritizationFee), ...ixs];
|
||||
}
|
||||
|
|
33
yarn.lock
33
yarn.lock
|
@ -30,17 +30,26 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@blockworks-foundation/mango-v4-settings@^0.2.16":
|
||||
version "0.2.16"
|
||||
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4-settings/-/mango-v4-settings-0.2.16.tgz#63f03c13d1677461ea42d3b1e9171042830c6cdc"
|
||||
integrity sha512-9GCRZkVqTQsEUkqx/488pmSWM8I74Ght5AEzMPsuS5XlRGWIDjytWoBE0Y9wFVPrnRw2THTj0ZQft+vDwCYDCA==
|
||||
dependencies:
|
||||
bn.js "^5.2.1"
|
||||
eslint-config-prettier "^9.0.0"
|
||||
|
||||
"@colors/colors@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
"@coral-xyz/anchor@^0.26.0", "@coral-xyz/anchor@^0.27.0":
|
||||
version "0.27.0"
|
||||
resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.27.0.tgz#621e5ef123d05811b97e49973b4ed7ede27c705c"
|
||||
integrity sha512-+P/vPdORawvg3A9Wj02iquxb4T0C5m4P6aZBVYysKl4Amk+r6aMPZkUhilBkD6E4Nuxnoajv3CFykUfkGE0n5g==
|
||||
"@coral-xyz/anchor@^0.26.0", "@coral-xyz/anchor@^0.28.1-beta.2":
|
||||
version "0.28.1-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.28.1-beta.2.tgz#4ddd4b2b66af04407be47cf9524147793ec514a0"
|
||||
integrity sha512-xreUcOFF8+IQKWOBUrDKJbIw2ftpRVybFlEPVrbSlOBCbreCWrQ5754Gt9cHIcuBDAzearCDiBqzsGQdNgPJiw==
|
||||
dependencies:
|
||||
"@coral-xyz/borsh" "^0.27.0"
|
||||
"@coral-xyz/borsh" "^0.28.0"
|
||||
"@noble/hashes" "^1.3.1"
|
||||
"@solana/web3.js" "^1.68.0"
|
||||
base64-js "^1.5.1"
|
||||
bn.js "^5.1.2"
|
||||
|
@ -50,16 +59,15 @@
|
|||
cross-fetch "^3.1.5"
|
||||
crypto-hash "^1.3.0"
|
||||
eventemitter3 "^4.0.7"
|
||||
js-sha256 "^0.9.0"
|
||||
pako "^2.0.3"
|
||||
snake-case "^3.0.4"
|
||||
superstruct "^0.15.4"
|
||||
toml "^3.0.0"
|
||||
|
||||
"@coral-xyz/borsh@^0.27.0":
|
||||
version "0.27.0"
|
||||
resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.27.0.tgz#700c647ea5262b1488957ac7fb4e8acf72c72b63"
|
||||
integrity sha512-tJKzhLukghTWPLy+n8K8iJKgBq1yLT/AxaNd10yJrX8mI56ao5+OFAKAqW/h0i79KCvb4BK0VGO5ECmmolFz9A==
|
||||
"@coral-xyz/borsh@^0.28.0":
|
||||
version "0.28.0"
|
||||
resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.28.0.tgz#fa368a2f2475bbf6f828f4657f40a52102e02b6d"
|
||||
integrity sha512-/u1VTzw7XooK7rqeD7JLUSwOyRSesPUk0U37BV9zK0axJc1q0nRbKFGFLYCQ16OtdOJTTwGfGp11Lx9B45bRCQ==
|
||||
dependencies:
|
||||
bn.js "^5.1.2"
|
||||
buffer-layout "^1.2.0"
|
||||
|
@ -1094,6 +1102,11 @@ eslint-config-prettier@^7.2.0:
|
|||
resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz"
|
||||
integrity sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==
|
||||
|
||||
eslint-config-prettier@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f"
|
||||
integrity sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==
|
||||
|
||||
eslint-scope@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"
|
||||
|
|
Loading…
Reference in New Issue