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 commit 2adc0339dc)

* 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 commit 40ad0b7b66)

* Jupiter: ensure source account is initialized

Backport of 9b224eae1b / #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 commit 0f10cb4d92)

* Jupiter: Ensure source account is initialized (#721)

(cherry picked from commit 9b224eae1b)

* 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:
microwavedcola1 2023-10-19 10:03:07 +02:00 committed by GitHub
parent b123a3c5ae
commit edaf874174
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1545 additions and 258 deletions

2
.gitignore vendored
View File

@ -14,3 +14,5 @@ yarn-error.log
.idea
.vscode
err-txs

View File

@ -129,6 +129,7 @@ impl Rpc {
None,
TransactionBuilderConfig {
prioritization_micro_lamports: Some(5),
compute_budget_per_instruction: Some(250_000),
},
))
}

View File

@ -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();

View File

@ -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,

View File

@ -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),
},
);

View File

@ -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 =

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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),
},
);

View File

@ -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?;

View File

@ -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"
},

View File

@ -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

View File

@ -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) {

View File

@ -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}`,
);

View File

@ -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',
);

View File

@ -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;
};

View File

@ -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 };
};

View File

@ -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,
);
}
}

View File

@ -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;
}
};

View File

@ -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) >

View File

@ -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')) {

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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> {

View File

@ -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,

View File

@ -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;

View File

@ -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,
) {}
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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(

View File

@ -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!`,
);
}
}
}

View File

@ -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'),
};

39
ts/client/src/error.ts Normal file
View File

@ -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;
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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 {

View File

@ -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];
}

View File

@ -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"